Compare commits

...

490 Commits

Author SHA1 Message Date
Mads Marquart
856962af63 Bump version: 1.7.3 → 1.8.0 2019-08-28 10:58:46 +02:00
Mads Marquart
7c68a29181 Stop using Client.graphql_request internally 2019-07-25 23:32:17 +02:00
Mads Marquart
2f4e3f2bb1 Remove Client._generatePayload
Make Client._get and Client._post require a query input
2019-07-25 20:20:26 +02:00
Mads Marquart
0389b838bc Merge pull request #455 from carpedm20/add-spell-check
Add spell checking.

Use sphinxcontrib-spelling to fix documentation and docstring spelling errors.
2019-07-25 18:51:53 +02:00
Mads Marquart
441f53e382 Merge pull request #454 from carpedm20/google-style-docstrings
Google docstring style
2019-07-24 21:56:33 +02:00
Mads Marquart
83c45dcf40 Fix spelling / typesetting in various places 2019-07-24 16:18:15 +02:00
Mads Marquart
cc9d81a39e Fix spelling mistakes in documentation 2019-07-24 16:18:15 +02:00
Mads Marquart
edf14cfd84 Add and configure sphinxcontrib-spelling 2019-07-24 16:17:36 +02:00
Mads Marquart
ee79969eda Delete docs/robots.txt
Introduced in a2930b4, but I found out you could deprecate the doc url at /en/master/ using the ReadTheDocs web configuration
2019-07-24 16:15:31 +02:00
Mads Marquart
dbb20b1fdc Convert various directives to Google style sections 2019-07-24 13:45:33 +02:00
Mads Marquart
beee209249 Convert :return: / :rtype: roles to Returns sections 2019-07-24 13:45:33 +02:00
Mads Marquart
d6876ce13b Convert :raises: roles to Raises sections 2019-07-24 13:43:34 +02:00
Mads Marquart
ed05d16a31 Move :type: roles into the Args sections 2019-07-24 13:43:34 +02:00
Mads Marquart
3806f01d2f Convert :param: roles to Args sections 2019-07-24 13:43:30 +02:00
Mads Marquart
5b69ced1e8 Add ability to use Google style docstrings
Use and configure the `napoleon` Sphinx extension
2019-07-24 13:43:02 +02:00
Mads Marquart
6b07f1d8b9 Fix first line of docstrings
- Use the imperative sense
- Use trailing dot
- Omit leading newline
- Grammar / vocabulary fixes
2019-07-24 13:43:01 +02:00
Przemek
700cf14a50 Add fetchThreadImages (#434) 2019-07-24 13:40:00 +02:00
Mads Marquart
1b08243cd2 Fix TODO entries showing file paths of the build system 2019-07-24 00:33:55 +02:00
Mads Marquart
a0b978004c Bump version: 1.7.2 → 1.7.3 2019-07-20 17:09:03 +02:00
Mads Marquart
efc8776e70 Fix login check, close #446
Facebook changed something internally, so that the redirected url is no longer always "/home.php", but instead sometimes just "/"
2019-07-20 17:01:54 +02:00
Szczepan Wiśniowski
915f9a3782 Add heart reaction (#445) 2019-07-20 16:21:44 +02:00
Mads Marquart
e136d77ade Fix 2FA login error, closes #442, replaces #443 2019-07-20 16:00:32 +02:00
Mads Marquart
04aec15833 Fix documentation badge 2019-07-04 00:43:34 +02:00
Mads Marquart
dd5e1024db Bump version: 1.7.1 → 1.7.2 2019-07-04 00:34:11 +02:00
Mads Marquart
31d13f8fae Fix #441, introduced in bc551a6 2019-07-04 00:33:08 +02:00
Mads Marquart
19b4d929e2 Add bump2version (to avoid mistakes like pushing wrong tag names) 2019-07-04 00:25:05 +02:00
Mads Marquart
27e5d1baae Bump version: 1.7.0 -> 1.7.1 2019-07-03 23:59:45 +02:00
Mads Marquart
3a0b9867bc Merge pull request #440 from carpedm20/fix-docs
Fix and clean up documentation
2019-07-03 23:55:16 +02:00
Mads Marquart
a9c681818a Enable strict/explicit code highlighting 2019-07-03 23:42:32 +02:00
Mads Marquart
d279c96dd5 Make docs parsing "nitpicky" 2019-07-03 23:18:02 +02:00
Mads Marquart
d30589d1fa Add rst_prolog to docs/conf.py 2019-07-03 17:46:42 +02:00
Mads Marquart
47c744e5e2 Fix reST any roles/references 2019-07-03 17:35:38 +02:00
Mads Marquart
708869ea93 Include missing models in auto-generated API docs 2019-07-03 17:19:11 +02:00
Mads Marquart
8b47bf3e5d Add instructions for installing with pip > 19.0 2019-07-03 17:16:25 +02:00
Mads Marquart
a2930b4386 Deprecate the doc url at /en/master/ in favor of /en/latest/ 2019-07-03 17:15:21 +02:00
Mads Marquart
2dc93ed18b Add .readthedocs.yml 2019-07-03 15:18:11 +02:00
Mads Marquart
2bd08c8254 Update Sphinx to version 2.0 2019-07-03 14:22:09 +02:00
Mads Marquart
81278ed553 Remove doc configuration entries that are set to the default 2019-07-03 14:21:40 +02:00
Mads Marquart
589cec66e1 Refactor doc files to match format generated by sphinx-quickstart 2019-07-03 14:21:25 +02:00
Mads Marquart
281a20f56a Fix dependency pinning 2019-07-03 12:55:13 +02:00
Mads Marquart
ae8d205dbe Loosely pin dependencies 2019-07-03 11:17:53 +02:00
Mads Marquart
1e6222f46a Optimize BeautifulSoup input field parsing 2019-07-03 11:09:41 +02:00
Mads Marquart
4f2a24848e Use default black "exclude" string 2019-07-03 11:05:16 +02:00
Mads Marquart
e670c80971 Merge pull request #439 from carpedm20/graphql-cleanup
Clean up GraphQL helpers
2019-07-02 19:52:16 +02:00
Mads Marquart
ba7572eddd Merge branch 'master' into graphql-cleanup 2019-07-02 19:17:53 +02:00
Mads Marquart
a5c6fac976 Merge pull request #438 from carpedm20/explicit-error-handling
Add more explicit error handling and improve error handling in general
2019-07-02 18:57:47 +02:00
Mads Marquart
1293814c3a Remove GraphQL object in favor of helper functions 2019-07-02 18:26:35 +02:00
Mads Marquart
1b2aeb01ce Move GraphQL constants into the module 2019-07-02 18:23:29 +02:00
Mads Marquart
cab8abd1a0 Properly namespace GraphQL utility functions 2019-07-02 18:21:00 +02:00
Mads Marquart
edda2386fb Merge pull request #436 from carpedm20/clean-up-requests
Normalize a lot of request parsing
2019-07-02 18:08:19 +02:00
Mads Marquart
b0ad5f6097 Merge pull request #435 from carpedm20/state-refactor
Move state handling into new State model
2019-07-02 18:04:10 +02:00
Mads Marquart
6862bd7be3 Handle errors in payload explicitly 2019-07-02 17:52:10 +02:00
Mads Marquart
bc551a63c2 Improve GraphQL error handling 2019-07-02 17:50:33 +02:00
Mads Marquart
c9f11b924d Add more explicit error handling 2019-07-02 17:32:35 +02:00
Mads Marquart
3236ea5b97 Improve state refresh handler 2019-07-02 17:23:42 +02:00
Mads Marquart
794696d327 Improve payload error handling 2019-07-02 17:23:42 +02:00
Mads Marquart
7345de149a Improve HTTP error handling 2019-07-02 17:23:42 +02:00
Mads Marquart
4fdf0bbc57 Remove JSON conversion from _util.check_request 2019-07-02 17:23:42 +02:00
Mads Marquart
d17f741f97 Refactor _util.check_json 2019-07-02 17:23:42 +02:00
Mads Marquart
4a898b3ff5 Use Client._payload_post helper where relevant 2019-07-02 15:52:02 +02:00
Mads Marquart
7f84ca8d0c Add Client._payload_post helper 2019-07-02 15:50:58 +02:00
Mads Marquart
c3a974a495 Refactor _util.check_request 2019-07-02 15:34:23 +02:00
Mads Marquart
5b57d49a3e Remove Client._postFile 2019-07-02 15:14:02 +02:00
Mads Marquart
7af83c04c0 Remove as_json parameter
The requests that didn't need this parameter were moved to the State model
2019-07-01 22:53:26 +02:00
Mads Marquart
b5ba338f86 Remove fix_request parameter
The requests that don't need this parameter is handled in the State model
2019-07-01 22:49:21 +02:00
Mads Marquart
50bfeb92b2 Add fix_request=True and as_json=True to missing requests
I've tested, these endpoints actually all return JSON data
2019-07-01 22:47:05 +02:00
Mads Marquart
8d41ea5bfd Use POST in Client.fetchImageUrl
Reduces the amount of different request methods we're using.

Not really sure whether this is actually the best option:
- Each request includes `fb_dtsg` and such, so using POST everywhere might be the more secure option?
- But at the same time, the request is more opaque, and harder to debug (urllib3 logs all request urls automatically, so using GET would make that easy)
2019-07-01 18:43:00 +02:00
Mads Marquart
b10b14c8e9 Update url in Client.removeFriend 2019-07-01 18:23:03 +02:00
Mads Marquart
144e81bd46 Add Python 2 support 2019-07-01 13:40:15 +02:00
Mads Marquart
230c849b60 Always create the State object in a valid state 2019-07-01 13:31:42 +02:00
Mads Marquart
466f27a8c5 Move login check code into State 2019-07-01 13:31:42 +02:00
Mads Marquart
dc12e01fc7 Move logout code to State 2019-07-01 13:31:42 +02:00
Mads Marquart
d0e9a7f693 Move login/2fa code to State 2019-07-01 13:31:42 +02:00
Mads Marquart
1ba21e03c6 Handle headers in State 2019-07-01 13:31:42 +02:00
Mads Marquart
bcc8b44bb5 Handle ssl verification in State 2019-07-01 13:31:42 +02:00
Mads Marquart
b01b371c66 Refactor session cookie handling into State 2019-07-01 13:31:42 +02:00
Mads Marquart
94a0f6b3df Move client session into State 2019-07-01 13:31:42 +02:00
Mads Marquart
5df10ecc31 Remove _cleanGet and _cleanPost Client methods 2019-07-01 13:31:42 +02:00
Mads Marquart
56786406ec Refactor most of _postLogin into the State model 2019-07-01 13:31:42 +02:00
Mads Marquart
a4268f36cf Move logout h into the State model 2019-07-01 13:31:42 +02:00
Mads Marquart
8e7afa2edf Move request counter into State model 2019-07-01 13:31:30 +02:00
Mads Marquart
f07122d446 Move request payload into State model 2019-07-01 13:30:29 +02:00
Mads Marquart
78c307780b Clean up a few utility functions 2019-06-29 20:40:11 +02:00
Mads Marquart
ad705d544a Merge pull request #433 from carpedm20/remove-req-url-model
Remove ReqUrl model
2019-06-29 20:24:20 +02:00
Mads Marquart
77f28315c9 Inline urls from ReqUrl 2019-06-29 20:14:49 +02:00
Mads Marquart
e0754031ad Extract pull channel handling from ReqUrl 2019-06-29 20:10:55 +02:00
Mads Marquart
f97d36b41f Add ability to specify urls relative to www.facebook.com 2019-06-29 20:05:16 +02:00
Mads Marquart
bb2afe8e40 Remove redundant timeout parameter 2019-06-23 18:25:30 +02:00
Mads Marquart
faa0383af3 Remove unnecessary default payload attributes
This has been fairly thoroughly tested on all URLs, so it should be safe to do
2019-06-23 18:08:25 +02:00
Mads Marquart
e1e988272b Version up, thanks to @kapi2289
- Removed / privatized a lot of undocumented functionality, which might break some users application if they were relying on those (hence the minor version bump). See #432 for further reasoning
- Add `Client.forwardAttachment` and `Message.forwarded` attribute, see #420
- Fix parsing subattachments with no target, see #412
- Lots of other refactoring, aka. work on the transition towards v2
2019-06-23 18:02:05 +02:00
Mads Marquart
b159f04a6b Merge pull request #432 from carpedm20/undocumented-breaking-changes
Undocumented breaking changes
2019-06-23 17:51:37 +02:00
Mads Marquart
d91a7ea9e3 Remove internal stuff from _graphql in __init__.py 2019-06-23 17:44:03 +02:00
Mads Marquart
8056f3399e Privatize Client.req_url
Undocumented
2019-06-23 17:40:20 +02:00
Mads Marquart
fd9aa7ee90 Remove client.py
Undocumented
2019-06-23 17:38:42 +02:00
Mads Marquart
53c19f473b Remove utils.py
The file, and contained functions, were explicitly documented as something you couldn't rely on for backwards compatibility
2019-06-23 17:38:27 +02:00
Mads Marquart
78b5f05729 Fix search when the result is empty
Return an empty result instead of raising
2019-05-01 23:50:23 +02:00
Mads Marquart
f689376830 Fix check_json
The payload is sometimes a list
2019-05-01 23:49:29 +02:00
Mads Marquart
d244856b41 Fix setting forwarded when creating Message objects 2019-05-01 23:18:12 +02:00
Kacper Ziubryniewicz
3cd0f3a9a7 Add missing mid parameter in message parsing (#426) 2019-05-01 19:32:04 +02:00
Mads Marquart
f480d68b57 Fix subattachment target parsing, fixes #412 2019-04-25 23:26:04 +02:00
Mads Marquart
db2bda1f9b Fix ShareAttachment GraphQL source parsing
Fixes #418
2019-04-25 23:26:03 +02:00
Kacper Ziubryniewicz
f834c01921 Fix sending remote files with URLs containing GET parameters or hashtags (#423) 2019-04-25 22:50:32 +02:00
Mads Marquart
f945fa80b3 Merge pull request #419 from carpedm20/remove-client-variables
Clean up `Client` variables
2019-04-25 22:45:45 +02:00
Kacper Ziubryniewicz
70faa86e34 Add forwarding attachments (#420)
- Add ability to forward attachments and `Message.forwarded` attribute
- Improve error handling for a lot of client methods, including, but not limited to:
    - `fetchAllUsers`
    - `searchForMessageIDs`
    - `search`
    - `fetchThreadInfo` and siblings
    - `fetchUnread`
    - `fetchUnseen`
    - `fetchPollOptions`
    - `fetchPlanInfo`
    - `send` and siblings
    - File uploads
2019-04-25 22:03:03 +02:00
Mads Marquart
61502ed32a Merge pull request #406 from carpedm20/refactor-model-parsing
Refactor model parsing
2019-04-25 21:06:26 +02:00
Mads Marquart
bfca20bb12 Privatize req_counter, payloadDefault and seq client variables
These have complicated semantics, and so are hopefully not depended on externally
2019-04-17 23:24:01 +02:00
Mads Marquart
0fd86d05a1 Privatize sticky and pool client variables
These have complicated semantics, and so are hopefully not depended on externally
2019-04-17 23:22:54 +02:00
Mads Marquart
c688d64062 Make Client.uid read-only
Modifying `uid` was previously documented as giving undefined behaviour, now it'll throw an error
2019-04-17 23:22:23 +02:00
Mads Marquart
2f973f129d Privatize default_thread_X client variables
We have a setter method for them, so there should be no need to access these directly!
2019-04-17 23:20:08 +02:00
Mads Marquart
9b81365b0a Privatize fb_h and client_id variables
These are sparsely used and badly named, so probably not externally depended on externally
2019-04-17 23:20:08 +02:00
Mads Marquart
a079797fca Remove email/password client variables 2019-04-17 23:18:37 +02:00
Mads Marquart
6ab298f6e8 Remove temporary _postLogin variables from the client 2019-04-17 23:18:37 +02:00
Mads Marquart
a159999879 Remove redundant client variables 2019-04-17 23:18:37 +02:00
Mads Marquart
a71835a5b8 Version up, thanks to @kapi2289 and @LukasOndrejka 2019-04-17 22:58:37 +02:00
Mads Marquart
86a6e07804 Merge branch 'master' into refactor-model-parsing 2019-04-17 22:42:20 +02:00
Kacper Ziubryniewicz
73c6be1969 Add message parameter to sending location (#416) 2019-04-17 22:35:04 +02:00
Kacper Ziubryniewicz
7db7868d2b Fix forwarding replied messages (#417) 2019-04-17 22:34:19 +02:00
Mads Marquart
18ec1f5680 Merge branch 'master' into refactor-model-parsing 2019-04-17 22:06:07 +02:00
Mads Marquart
8e65074b11 Hotfixes
Fix mistakes introduced in #415
2019-04-17 21:56:50 +02:00
Mads Marquart
d720438aef Merge pull request #415 from carpedm20/cleanup
Cleanup
2019-04-17 21:11:18 +02:00
Mads Marquart
ec0e3a91d1 Merge branch 'master' into cleanup 2019-04-17 20:50:08 +02:00
Mads Marquart
48e7203ca6 Rename internal variable 2019-04-17 20:47:49 +02:00
Mads Marquart
4f76b79629 Merge pull request #409 from kapi2289/message-reply
Add replying to messages
2019-04-17 20:44:30 +02:00
Mads Marquart
1eeae78a9f Small refactoring
The `muteX` methods return values are now checked using `check_request`, `seq` is now parsed in `_parseMessage` and a few other things.
2019-03-31 00:16:36 +01:00
Mads Marquart
bc27f756ed Split long strings, use format when creating strings 2019-03-31 00:11:20 +01:00
Mads Marquart
6302d5fb8b Split overly nested calls 2019-03-31 00:07:29 +01:00
Mads Marquart
24e238c425 Remove superfluous whitespace 2019-03-31 00:02:49 +01:00
Mads Marquart
070f57fcc4 Refactor _graphql away 2019-03-30 21:20:20 +01:00
Kacper Ziubryniewicz
a4ce45e9b0 Add detecting replied messages while listening 2019-03-29 21:09:19 +01:00
Kacper Ziubryniewicz
a3efa7702a Add possibility to reply to messages
and to (partly) fetch the replied messages
2019-03-23 21:26:43 +01:00
LukasOndrejka
d7a5d00439 Add new colors (#393)
Color names are from https://www.htmlcsscolor.com/
2019-03-12 18:15:11 +01:00
Mads Marquart
6636d49cc0 Remove graphql.py 2019-03-10 20:30:22 +01:00
Mads Marquart
8e6ee4636e Move gender dict into _user.py 2019-03-10 20:25:02 +01:00
Mads Marquart
71f19dd3c7 Move fetchAllUsers parsing into User._from_all_fetch 2019-03-10 20:22:56 +01:00
Mads Marquart
e166b472c5 Move message pull parsing into Message._from_pull 2019-03-10 20:10:19 +01:00
Mads Marquart
28c867a115 Simplify _graphql.py imports 2019-03-10 19:54:36 +01:00
Mads Marquart
f20a04b2a0 Move graphql_to_message -> Message._from_graphql 2019-03-10 19:50:06 +01:00
Mads Marquart
1f961b2ca7 Move thread parser dispatching into _client.py
I know, it's not pretty, but it doesn't belong in _graphql.py either
2019-03-10 19:39:22 +01:00
Mads Marquart
e579e0c767 Move graphql_to_quick_reply into _quick_reply.py 2019-03-10 19:35:07 +01:00
Mads Marquart
6693ec9c36 Move graphql_to_extensible_attachment into _message.py 2019-03-10 19:33:58 +01:00
Mads Marquart
53856a3622 Move attachment and subattachment dispatching to _file.py 2019-03-10 19:26:01 +01:00
Mads Marquart
0b99238676 Move subattachment parsing to the respective models 2019-03-10 19:19:50 +01:00
Mads Marquart
cb2c68e25a Move graphql_to_page -> Page._from_graphql 2019-03-10 17:36:41 +01:00
Mads Marquart
fd5553a9f5 Move graphql_to_group -> Group._from_graphql 2019-03-10 17:33:46 +01:00
Mads Marquart
60ebbd87d8 Move graphql_to_thread user parsing to User._from_thread_fetch 2019-03-10 17:26:47 +01:00
Mads Marquart
3a5185fcc8 Move graphql_to_user -> User._from_graphql 2019-03-10 17:21:06 +01:00
Mads Marquart
ce469d5e5a Move get_customization_info -> Thread._parse_customization_info 2019-03-10 17:02:03 +01:00
Mads Marquart
4f0f126e48 Make Github Releases deploy in the published state 2019-03-10 16:45:16 +01:00
Mads Marquart
94c30a2440 Merge pull request #405 from carpedm20/private-api
Privatize `client`, `utils` and `graphql` submodules
2019-03-10 16:42:48 +01:00
Mads Marquart
1460b2f421 Version up, thanks to @oneblue and @darylkell 2019-03-10 16:33:44 +01:00
Mads Marquart
968223690e Move plan parsing to the Plan model
- Add `GuestStatus` enum
- Add `Plan.guests` property
- Made `Plan.going`, `Plan.declined` and `Plan.invited` property accessors to `Plan.guests`
2019-03-10 16:21:22 +01:00
Mads Marquart
789d9d8ca1 Split graphql_to_attachment into smaller methods 2019-03-07 21:22:56 +01:00
Mads Marquart
2ce99a2c44 Split graphql_to_extensible_attachment into smaller methods 2019-03-07 20:50:14 +01:00
Mads Marquart
ee207e994f Move graphql_to_live_location -> LiveLocationAttachment._from_pull 2019-03-07 20:17:29 +01:00
Mads Marquart
c374aca890 Fix _util exception import 2019-03-07 19:59:25 +01:00
Mads Marquart
c28ca58537 Add missing attributes to Poll and PollOption __init__ 2019-03-07 19:58:24 +01:00
Mads Marquart
0578ea2c3c Move graphql_to_poll -> Poll._from_graphql 2019-03-07 19:52:29 +01:00
Mads Marquart
e51ce99c1a Move graphql_to_poll_option -> PollOption._from_graphql 2019-03-07 19:47:36 +01:00
Mads Marquart
3440039610 Move graphql_to_sticker -> Sticker._from_graphql 2019-03-07 19:07:00 +01:00
Mads Marquart
279f637c75 Move graphql_color_to_enum -> ThreadColor._from_graphql 2019-03-07 18:54:38 +01:00
Mads Marquart
d940b64517 Move enum_extend_if_invalid -> Enum._extend_if_invalid 2019-03-07 18:42:58 +01:00
Mads Marquart
403870e39e Move emojisize pull parsing into the model 2019-03-07 18:13:05 +01:00
Mads Marquart
0383d613e6 Move ActiveStatus pull parsing into the model 2019-03-07 18:12:37 +01:00
Mads Marquart
40e9825ee0 Add deprecated public graphql module 2019-02-24 23:21:17 +01:00
Mads Marquart
ab9ca94181 Rename graphql.py -> _graphql.py 2019-02-24 23:17:36 +01:00
Mads Marquart
0f99a23af7 Add deprecated public utils module 2019-02-24 23:16:40 +01:00
Mads Marquart
bc5163adaf Rename utils.py -> _util.py 2019-02-24 23:15:12 +01:00
Mads Marquart
0561718917 Import utils.py and graphql.py directly into the global module 2019-02-24 23:08:23 +01:00
Mads Marquart
c1861627fb Make deprecated public client module 2019-02-24 23:06:29 +01:00
Mads Marquart
e5eccab871 Rename client.py -> _client.py 2019-02-24 23:01:26 +01:00
Mads Marquart
27f76ba659 Merge pull request #400 from carpedm20/pull-delta-refactor
Move pull delta parsing into separate method
2019-02-24 20:52:26 +01:00
Mads Marquart
589117b9e7 Move pull delta parsing into _parseDelta (commit 2) 2019-02-24 20:45:44 +01:00
Mads Marquart
80300cd160 Move pull delta parsing into _parseDelta (commit 1) 2019-02-24 20:45:01 +01:00
Mads Marquart
76171408cc Merge pull request #399 from carpedm20/attrs
Use attrs to declare our models
2019-02-24 20:24:21 +01:00
Mads Marquart
c1800a174f Update minimum attrs version 2019-02-24 20:18:11 +01:00
Mads Marquart
8ae8435940 Use attrs, to remove verbose __init__ and __repr__ methods
Backwards compatibility is strictly preserved in `__init__`, including parameter names, defaults and position. Whenever that's difficult using `attrs`, the custom `__init__` is kept instead (for the time being).

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

7
.bumpversion.cfg Normal file
View File

@@ -0,0 +1,7 @@
[bumpversion]
current_version = 1.8.0
commit = True
tag = True
[bumpversion:file:fbchat/__init__.py]

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`

12
.gitignore vendored
View File

@@ -8,9 +8,11 @@
# Packages
*.egg
*.egg-info
*.dist-info
dist
build
eggs
.eggs
parts
bin
var
@@ -24,7 +26,13 @@ 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
tests.data
.pytest_cache
# Virtual environment
venv/
.venv*/

18
.readthedocs.yml Normal file
View File

@@ -0,0 +1,18 @@
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
version: 2
formats:
- pdf
- htmlzip
python:
version: 3.6
install:
- path: .
extra_requirements:
- docs
# Build documentation in the docs/ directory with Sphinx
sphinx:
configuration: docs/conf.py
fail_on_warning: true

61
.travis.yml Normal file
View File

@@ -0,0 +1,61 @@
sudo: false
language: python
python: 3.6
cache: pip
before_install: pip install flit
# Use `--deps production` so that we don't install unnecessary dependencies
install: flit install --deps production --extras test
script: pytest -m offline
jobs:
include:
- python: 2.7
before_install:
- sudo apt-get -y install python3-pip python3-setuptools
- sudo pip3 install flit
install: flit install --python python --deps production --extras test
- python: 3.4
- python: 3.5
- python: 3.6
- python: 3.7
dist: xenial
sudo: required
- python: pypy3.5
- name: Lint
before_install: skip
install: pip install black
script: black --check --verbose .
- stage: deploy
name: GitHub Releases
if: tag IS present
install: skip
script: flit build
deploy:
provider: releases
api_key: $GITHUB_OAUTH_TOKEN
file_glob: true
file: dist/*
skip_cleanup: true
draft: false
on:
tags: true
- stage: deploy
name: PyPI
if: tag IS present
install: skip
script: skip
deploy:
provider: script
script: flit publish
on:
tags: true
notifications:
email:
on_success: never
on_failure: change

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

38
CONTRIBUTING.rst Normal file
View File

@@ -0,0 +1,38 @@
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``.
Development Environment
-----------------------
You can use `flit` to install the package as a symlink:
.. code-block::
$ # *nix:
$ flit install --symlink
$ # Windows:
$ flit install --pth-file
After that, you can ``import`` the module as normal.
Before committing, you should run ``black .`` in the main directory, to format your code.
Testing Environment
-------------------
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 +0,0 @@
include LICENSE.txt
include MANIFEST.in
include README.rst
include setup.py

View File

@@ -1,32 +1,48 @@
fbchat: Facebook Chat (Messenger) for Python
============================================
``fbchat``: Facebook Chat (Messenger) for Python
================================================
.. image:: https://img.shields.io/badge/license-BSD-blue.svg
:target: LICENSE.txt
:alt: License: BSD
:target: https://github.com/carpedm20/fbchat/tree/master/LICENSE
:alt: License: BSD 3-Clause
.. image:: https://img.shields.io/badge/python-2.7%2C%203.4%2C%203.5%2C%203.6-blue.svg
.. image:: https://img.shields.io/badge/python-2.7%2C%203.4%2C%203.5%2C%203.6%203.7%20pypy-blue.svg
:target: https://pypi.python.org/pypi/fbchat
:alt: Supported python versions: 2.7, 3.4, 3.5 and 3.6
:alt: Supported python versions: 2.7, 3.4, 3.5, 3.6, 3.7 and pypy
.. image:: https://readthedocs.org/projects/fbchat/badge/?version=master
.. image:: https://readthedocs.org/projects/fbchat/badge/?version=latest
: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
.. image:: https://img.shields.io/badge/code%20style-black-000000.svg
:target: https://github.com/ambv/black
:alt: Code style
Facebook Chat (`Messenger <https://www.facebook.com/messages/>`__) for Python.
This project was inspired by `facebook-chat-api <https://github.com/Schmavery/facebook-chat-api>`__.
**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 <examples>`__
or jump right into the code by viewing the `examples <https://github.com/carpedm20/fbchat/tree/master/examples>`__
Installation:
.. code-block:: console
.. code-block::
$ pip install fbchat
You can also install from source if you have ``pip>=19.0``:
.. code-block::
$ git clone https://github.com/carpedm20/fbchat.git
$ pip install fbchat
Maintainer
----------

View File

@@ -3,8 +3,7 @@
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = python3.6 -msphinx
SPHINXPROJ = fbchat
SPHINXBUILD = sphinx-build
SOURCEDIR = .
BUILDDIR = _build

View File

@@ -1,44 +1,79 @@
.. module:: fbchat
.. highlight:: python
.. _api:
.. Note: we're using () to hide the __init__ method where relevant
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 interract with Facebook.
You can extend this class, and overwrite the events, to provide custom event handling (mainly used while listening)
.. autoclass:: Client
.. autoclass:: Client(email, password, user_agent=None, max_tries=5, session_cookies=None, logging_level=logging.INFO)
:members:
Threads
-------
.. autoclass:: Thread()
.. autoclass:: ThreadType(Enum)
:undoc-members:
.. autoclass:: Page()
.. autoclass:: User()
.. autoclass:: Group()
.. _api_models:
Messages
--------
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:
.. autoclass:: Message
.. autoclass:: Mention
.. autoclass:: EmojiSize(Enum)
:undoc-members:
.. autoclass:: MessageReaction(Enum)
:undoc-members:
Exceptions
----------
.. _api_utils:
.. autoexception:: FBchatException()
.. autoexception:: FBchatFacebookError()
.. autoexception:: FBchatUserError()
Utils
-----
Attachments
-----------
These functions and values are used internally by fbchat, and are subject to change. Do **NOT** rely on these to be backwards compatible!
.. autoclass:: Attachment()
.. autoclass:: ShareAttachment()
.. autoclass:: Sticker()
.. autoclass:: LocationAttachment()
.. autoclass:: LiveLocationAttachment()
.. autoclass:: FileAttachment()
.. autoclass:: AudioAttachment()
.. autoclass:: ImageAttachment()
.. autoclass:: VideoAttachment()
.. autoclass:: ImageAttachment()
.. automodule:: fbchat.utils
:members:
Miscellaneous
-------------
.. autoclass:: ThreadLocation(Enum)
:undoc-members:
.. autoclass:: ThreadColor(Enum)
:undoc-members:
.. autoclass:: ActiveStatus()
.. autoclass:: TypingStatus(Enum)
:undoc-members:
.. autoclass:: QuickReply
.. autoclass:: QuickReplyText
.. autoclass:: QuickReplyLocation
.. autoclass:: QuickReplyPhoneNumber
.. autoclass:: QuickReplyEmail
.. autoclass:: Poll
.. autoclass:: PollOption
.. autoclass:: Plan
.. autoclass:: GuestStatus(Enum)
:undoc-members:

View File

@@ -1,191 +1,208 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# fbchat documentation build configuration file, created by
# sphinx-quickstart on Thu May 25 15:43:01 2017.
# Configuration file for the Sphinx documentation builder.
#
# 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.
# This file does only contain a selection of the most common options. For a
# full list see the documentation:
# http://www.sphinx-doc.org/en/master/config
# 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.
# -- Path setup --------------------------------------------------------------
import os
import sys
sys.path.insert(0, os.path.abspath('..'))
sys.path.insert(0, os.path.abspath(".."))
import fbchat
import tests
from fbchat import __copyright__, __author__, __version__, __description__
# -- Project information -----------------------------------------------------
project = fbchat.__name__
copyright = fbchat.__copyright__
author = fbchat.__author__
# The short X.Y version
version = fbchat.__version__
# The full version, including alpha/beta/rc tags
release = fbchat.__version__
# -- General configuration ------------------------------------------------
# -- General configuration ---------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
#
# needs_sphinx = '1.0'
needs_sphinx = "2.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'
"sphinx.ext.autodoc",
"sphinx.ext.intersphinx",
"sphinx.ext.todo",
"sphinx.ext.viewcode",
"sphinx.ext.napoleon",
"sphinxcontrib.spelling",
]
# 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'
templates_path = ["_templates"]
# 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
master_doc = "index"
# 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']
# This pattern also affects 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'
rst_prolog = ".. currentmodule:: " + project
# If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = True
# The reST default role (used for this markup: `text`) to use for all
# documents.
#
default_role = "any"
# Make the reference parsing more strict
#
nitpicky = True
# Prefer strict Python highlighting
#
highlight_language = "python3"
# If true, '()' will be appended to :func: etc. cross-reference text.
#
add_function_parentheses = False
# -- Options for HTML output ----------------------------------------------
# -- 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'
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 = {}
html_theme_options = {
"show_powered_by": False,
"github_user": "carpedm20",
"github_repo": project,
"github_banner": True,
"show_related": False,
}
# 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']
# Custom sidebar templates, must be a dictionary that maps document names
# to template names.
#
# The default sidebars (for documents that don't match any pattern) are
# defined by theme itself. Builtin themes are using these templates by
# default: ``['localtoc.html', 'relations.html', 'sourcelink.html',
# 'searchbox.html']``.
#
html_sidebars = {"**": ["sidebar.html", "searchbox.html"]}
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
#
html_show_sphinx = False
# If true, links to the reST sources are added to the pages.
#
html_show_sourcelink = False
# A shorter title for the navigation bar. Default is the same as html_title.
#
html_short_title = fbchat.__description__
# -- Options for HTMLHelp output ------------------------------------------
# -- Options for HTMLHelp output ---------------------------------------------
# Output file base name for HTML help builder.
htmlhelp_basename = project + 'doc'
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',
}
# -- Options for LaTeX output ------------------------------------------------
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
(master_doc, project + '.tex', title,
author, 'manual'),
]
latex_documents = [(master_doc, project + ".tex", fbchat.__title__, author, "manual")]
# -- Options for manual page output ---------------------------------------
# -- 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)
(master_doc, project, fbchat.__title__, [x.strip() for x in author.split(";")], 1)
]
# -- Options for Texinfo output -------------------------------------------
# -- 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'),
(
master_doc,
project,
fbchat.__title__,
author,
project,
fbchat.__description__,
"Miscellaneous",
)
]
# -- Options for Epub output -------------------------------------------------
# A list of files that should not be packed into the epub file.
epub_exclude_files = ["search.html"]
# -- Extension configuration -------------------------------------------------
# -- Options for autodoc extension ---------------------------------------
autoclass_content = "both"
autodoc_member_order = "bysource"
autodoc_default_options = {"members": True}
# -- Options for intersphinx extension ---------------------------------------
# Example configuration for intersphinx: refer to the Python standard library.
intersphinx_mapping = {'https://docs.python.org/3/': None}
intersphinx_mapping = {"https://docs.python.org/": None}
# -- Options for todo extension ----------------------------------------------
# If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = True
add_function_parentheses = False
todo_link_only = True
html_theme_options = {
'show_powered_by': False,
'github_user': 'carpedm20',
'github_repo': project,
'github_banner': True,
'show_related': False
}
# -- Options for napoleon extension ----------------------------------------------
html_sidebars = {
'**': ['sidebar.html', 'searchbox.html']
}
# Use Google style docstrings
napoleon_google_docstring = True
napoleon_numpy_docstring = False
html_show_sphinx = False
html_show_sourcelink = False
autoclass_content = 'init'
html_short_title = description
# napoleon_use_admonition_for_examples = False
# napoleon_use_admonition_for_notes = False
# napoleon_use_admonition_for_references = False
# -- Options for spelling extension ----------------------------------------------
spelling_word_list_filename = [
"spelling/names.txt",
"spelling/technical.txt",
"spelling/fixes.txt",
]
spelling_ignore_wiki_words = False
# spelling_ignore_acronyms = False
spelling_ignore_python_builtins = False
spelling_ignore_importable_modules = False

View File

@@ -1,16 +1,15 @@
.. 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
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`
This will show basic usage of ``fbchat``
.. literalinclude:: ../examples/basic_usage.py
@@ -18,7 +17,7 @@ This will show basic usage of `fbchat`
Interacting with Threads
------------------------
This will interract with the thread in every way `fbchat` supports
This will interact with the thread in every way ``fbchat`` supports
.. literalinclude:: ../examples/interract.py
@@ -31,8 +30,8 @@ This will show the different ways of fetching information about users and thread
.. literalinclude:: ../examples/fetch.py
Echobot
-------
``Echobot``
-----------
This will reply to any message with the same message
@@ -42,7 +41,7 @@ This will reply to any message with the same message
Remove Bot
----------
This will remove a user from a group if they write the message `Remove me!`
This will remove a user from a group if they write the message ``Remove me!``
.. literalinclude:: ../examples/removebot.py

View File

@@ -1,5 +1,3 @@
.. highlight:: python
.. module:: fbchat
.. _faq:
FAQ
@@ -8,10 +6,10 @@ FAQ
Version X broke my installation
-------------------------------
We try to provide backwards compatability where possible, but since we're not part of Facebook,
We try to provide backwards compatibility where possible, but since we're not part of Facebook,
most of the things may be broken at any point in time
Downgrade to an earlier version of fbchat, run this command
Downgrade to an earlier version of ``fbchat``, run this command
.. code-block:: sh
@@ -23,14 +21,14 @@ 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 ;)
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>`_.
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

View File

@@ -1,5 +1,3 @@
.. 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
@@ -8,8 +6,8 @@
.. 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
============================================
``fbchat``: Facebook Chat (Messenger) for Python
================================================
Release v\ |version|. (:ref:`install`)
@@ -30,14 +28,14 @@ This project was inspired by `facebook-chat-api <https://github.com/Schmavery/fa
**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:
Currently ``fbchat`` support Python 2.7, 3.4, 3.5 and 3.6:
`fbchat` works by emulating the browser.
``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
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,
@@ -46,9 +44,9 @@ Therefore, this API requires the credentials of a Facebook account.
.. 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.
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`
If you're already familiar with the basics of how Facebook works internally, go to :ref:`examples` to see example usage of ``fbchat``
Overview

View File

@@ -1,13 +1,14 @@
.. highlight:: sh
.. _install:
Installation
============
Pip Install fbchat
------------------
Install using pip
-----------------
To install fbchat, run this command::
To install ``fbchat``, run this command:
.. code-block:: sh
$ pip install fbchat
@@ -18,19 +19,25 @@ can guide you through the process.
Get the Source Code
-------------------
fbchat is developed on GitHub, where the code is
``fbchat`` is developed on GitHub, where the code is
`always available <https://github.com/carpedm20/fbchat>`_.
You can either clone the public repository::
You can either clone the public repository:
.. code-block:: sh
$ git clone git://github.com/carpedm20/fbchat.git
Or, download a `tarball <https://github.com/carpedm20/fbchat/tarball/master>`_::
Or, download a `tarball <https://github.com/carpedm20/fbchat/tarball/master>`_:
.. code-block:: sh
$ curl -OL https://github.com/carpedm20/fbchat/tarball/master
# optionally, zipball is also available (for Windows users).
# 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::
package, or install it into your site-packages easily:
.. code-block:: sh
$ python setup.py install

View File

@@ -1,12 +1,10 @@
.. 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 seperate file, in case e.g. someone looks over your shoulder while you're writing code.
``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
@@ -16,7 +14,7 @@ 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 fasion, overwrite :func:`Client.on2FACode`)::
(If you want to supply the code in another fashion, overwrite :func:`Client.on2FACode`)::
from fbchat import Client
from fbchat.models import *
@@ -26,9 +24,9 @@ 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
Though the second line, ``from fbchat.models import *``, is not strictly necessary 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`)
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`::
@@ -48,9 +46,9 @@ 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``.
:class:`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 differetiates between these two internally
This is required for many of ``fbchat``'s functions, since Facebook differentiates between these two internally
Searching for group chats and finding their ID can be done via. :func:`Client.searchForGroups`,
and searching for users is possible via. :func:`Client.searchForUsers`. See :ref:`intro_fetching`
@@ -87,7 +85,7 @@ 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`,
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::
@@ -100,17 +98,17 @@ This snippet shows how to send a message, and then use the returned ID to react
Interacting with Threads
------------------------
`fbchat` provides multiple functions for 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::
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`
You can see a full example showing all the possible thread interactions with ``fbchat`` by going to :ref:`examples`
.. _intro_fetching:
@@ -118,7 +116,7 @@ You can see a full example showing all the possible thread interactions with `fb
Fetching Information
--------------------
You can use `fbchat` to fetch basic information like user names, profile pictures, thread names and user IDs
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::
@@ -127,12 +125,12 @@ The following snippet will search for users by their name, take the first (and m
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))
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`
You can see a full example showing all the possible ways to fetch information with ``fbchat`` by going to :ref:`examples`
.. _intro_sessions:
@@ -140,8 +138,8 @@ You can see a full example showing all the possible ways to fetch information wi
Sessions
--------
`fbchat` provides functions to retrieve and set the session cookies.
This will enable you to store the session cookies in a seperate file, so that you don't have to login each time you start your script.
``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()
@@ -156,7 +154,7 @@ Or you can set the ``session_cookies`` on your initial login.
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
You session cookies can be just as valuable as you password, so store them with equal care
.. _intro_events:
@@ -164,13 +162,13 @@ Or you can set the ``session_cookies`` on your initial login.
Listening & Events
------------------
To use the listening functions `fbchat` offers (like :func:`Client.listen`),
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`
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::
@@ -194,7 +192,7 @@ The change was in the parameters that our `onMessage` method took: ``message_obj
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,
Therefore, for both backwards and forwards compatibility,
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

View File

@@ -5,21 +5,20 @@ pushd %~dp0
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=python -msphinx
set SPHINXBUILD=sphinx-build
)
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.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.http://sphinx-doc.org/

3
docs/spelling/fixes.txt Normal file
View File

@@ -0,0 +1,3 @@
premade
todo
emoji

3
docs/spelling/names.txt Normal file
View File

@@ -0,0 +1,3 @@
Facebook
GraphQL
GitHub

View File

@@ -0,0 +1,14 @@
iterables
timestamp
metadata
spam
spammy
admin
admins
unsend
unsends
unmute
spritemap
online
inbox
subclassing

View File

@@ -1,5 +1,3 @@
.. highlight:: sh
.. module:: fbchat
.. _testing:
Testing
@@ -15,7 +13,9 @@ To use the tests, copy ``tests/data.json`` to ``tests/my_data.json`` or type the
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 commandline (not including the `test_` prefix). Example::
If you only want to execute specific tests, pass the function names in the command line (not including the ``test_`` prefix). Example:
.. code-block:: sh
$ python tests.py sendMessage sessions sendEmoji
@@ -23,7 +23,3 @@ If you only want to execute specific tests, pass the function names in the comma
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

View File

@@ -1,5 +1,3 @@
.. highlight:: python
.. module:: fbchat
.. _todo:
Todo
@@ -11,11 +9,11 @@ This page will be periodically updated to show missing features and documentatio
Missing Functionality
---------------------
- Implement Client.searchForMessage
- This will use the graphql request API
- Implement ``Client.searchForMessage``
- This will use the GraphQL request API
- Implement chatting with pages properly
- Write better FAQ
- Explain usage of graphql
- Explain usage of GraphQL
Documentation

View File

@@ -3,10 +3,10 @@
from fbchat import Client
from fbchat.models import *
client = Client('<email>', '<password>')
client = Client("<email>", "<password>")
print('Own id: {}'.format(client.uid))
print("Own id: {}".format(client.uid))
client.send(Message(text='Hi me!'), thread_id=client.uid, thread_type=ThreadType.USER)
client.send(Message(text="Hi me!"), thread_id=client.uid, thread_type=ThreadType.USER)
client.logout()

View File

@@ -5,8 +5,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(author_id, thread_id)
self.markAsRead(author_id)
self.markAsDelivered(thread_id, message_object.uid)
self.markAsRead(thread_id)
log.info("{} from {} in {}".format(message_object, thread_id, thread_type.name))
@@ -14,5 +14,6 @@ class EchoBot(Client):
if author_id != self.uid:
self.send(message_object, thread_id=thread_id, thread_type=thread_type)
client = EchoBot("<email>", "<password>")
client.listen()

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,29 +1,25 @@
# -*- coding: UTF-8 -*-
"""Facebook Chat (Messenger) for Python
:copyright: (c) 2015 - 2019 by Taehoon Kim
:license: BSD 3-Clause, see LICENSE for more details.
"""
from __future__ import unicode_literals
from datetime import datetime
from .client import *
# These imports are far too general, but they're needed for backwards compatbility.
from .models import *
"""
fbchat
~~~~~~
from ._client import Client
from ._util import log # TODO: Remove this (from examples too)
Facebook Chat (Messenger) for Python
__title__ = "fbchat"
__version__ = "1.8.0"
__description__ = "Facebook Chat (Messenger) for Python"
:copyright: (c) 2015 by Taehoon Kim.
:license: BSD, see LICENSE for more details.
"""
__copyright__ = "Copyright 2015 - 2019 by Taehoon Kim"
__license__ = "BSD 3-Clause"
__author__ = "Taehoon Kim; Moreels Pieter-Jan; Mads Marquart"
__email__ = "carpedm20@gmail.com"
__copyright__ = 'Copyright 2015 - {} by Taehoon Kim'.format(datetime.now().year)
__version__ = '1.2.0'
__license__ = 'BSD'
__author__ = 'Taehoon Kim; Moreels Pieter-Jan; Mads Marquart'
__email__ = 'carpedm20@gmail.com'
__source__ = 'https://github.com/carpedm20/fbchat/'
__description__ = 'Facebook Chat (Messenger) for Python'
__all__ = [
'Client',
]
__all__ = ["Client"]

86
fbchat/_attachment.py Normal file
View File

@@ -0,0 +1,86 @@
# -*- coding: UTF-8 -*-
from __future__ import unicode_literals
import attr
from . import _util
@attr.s(cmp=False)
class Attachment(object):
"""Represents a Facebook attachment."""
#: The attachment ID
uid = attr.ib(None)
@attr.s(cmp=False)
class UnsentMessage(Attachment):
"""Represents an unsent message attachment."""
@attr.s(cmp=False)
class ShareAttachment(Attachment):
"""Represents a shared item (e.g. URL) attachment."""
#: ID of the author of the shared post
author = attr.ib(None)
#: Target URL
url = attr.ib(None)
#: Original URL if Facebook redirects the URL
original_url = attr.ib(None)
#: Title of the attachment
title = attr.ib(None)
#: Description of the attachment
description = attr.ib(None)
#: Name of the source
source = attr.ib(None)
#: URL of the attachment image
image_url = attr.ib(None)
#: URL of the original image if Facebook uses ``safe_image``
original_image_url = attr.ib(None)
#: Width of the image
image_width = attr.ib(None)
#: Height of the image
image_height = attr.ib(None)
#: List of additional attachments
attachments = attr.ib(factory=list, converter=lambda x: [] if x is None else x)
# Put here for backwards compatibility, so that the init argument order is preserved
uid = attr.ib(None)
@classmethod
def _from_graphql(cls, data):
from . import _file
url = data.get("url")
rtn = cls(
uid=data.get("deduplication_key"),
author=data["target"]["actors"][0]["id"]
if data["target"].get("actors")
else None,
url=url,
original_url=_util.get_url_parameter(url, "u")
if "/l.php?u=" in url
else url,
title=data["title_with_entities"].get("text"),
description=data["description"].get("text")
if data.get("description")
else None,
source=data["source"].get("text") if data.get("source") else None,
attachments=[
_file.graphql_to_subattachment(attachment)
for attachment in data.get("subattachments")
],
)
media = data.get("media")
if media and media.get("image"):
image = media["image"]
rtn.image_url = image.get("uri")
rtn.original_image_url = (
_util.get_url_parameter(rtn.image_url, "url")
if "/safe_image.php" in rtn.image_url
else rtn.image_url
)
rtn.image_width = image.get("width")
rtn.image_height = image.get("height")
return rtn

4057
fbchat/_client.py Normal file

File diff suppressed because it is too large Load Diff

26
fbchat/_core.py Normal file
View File

@@ -0,0 +1,26 @@
# -*- coding: UTF-8 -*-
from __future__ import unicode_literals
import logging
import aenum
log = logging.getLogger("client")
class Enum(aenum.Enum):
"""Used internally by ``fbchat`` to support enumerations"""
def __repr__(self):
# For documentation:
return "{}.{}".format(type(self).__name__, self.name)
@classmethod
def _extend_if_invalid(cls, value):
try:
return cls(value)
except ValueError:
log.warning(
"Failed parsing {.__name__}({!r}). Extending enum.".format(cls, value)
)
aenum.extend_enum(cls, "UNKNOWN_{}".format(value).upper(), value)
return cls(value)

60
fbchat/_exception.py Normal file
View File

@@ -0,0 +1,60 @@
# -*- coding: UTF-8 -*-
from __future__ import unicode_literals
class FBchatException(Exception):
"""Custom exception thrown by ``fbchat``.
All exceptions in the ``fbchat`` module inherits this.
"""
class FBchatFacebookError(FBchatException):
#: The error code that Facebook returned
fb_error_code = None
#: The error message that Facebook returned (In the user's own language)
fb_error_message = None
#: The status code that was sent in the HTTP response (e.g. 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 FBchatInvalidParameters(FBchatFacebookError):
"""Raised by Facebook if:
- Some function supplied invalid parameters.
- Some content is not found.
- Some content is no longer available.
"""
class FBchatNotLoggedIn(FBchatFacebookError):
"""Raised by Facebook if the client has been logged out."""
fb_error_code = "1357001"
class FBchatPleaseRefresh(FBchatFacebookError):
"""Raised by Facebook if the client has been inactive for too long.
This error usually happens after 1-2 days of inactivity.
"""
fb_error_code = "1357004"
fb_error_message = "Please try closing and re-opening your browser window."
class FBchatUserError(FBchatException):
"""Thrown by ``fbchat`` when wrong values are entered."""

301
fbchat/_file.py Normal file
View File

@@ -0,0 +1,301 @@
# -*- coding: UTF-8 -*-
from __future__ import unicode_literals
import attr
from ._attachment import Attachment
@attr.s(cmp=False)
class FileAttachment(Attachment):
"""Represents a file that has been sent as a Facebook attachment."""
#: URL where you can download the file
url = attr.ib(None)
#: Size of the file in bytes
size = attr.ib(None)
#: Name of the file
name = attr.ib(None)
#: Whether Facebook determines that this file may be harmful
is_malicious = attr.ib(None)
# Put here for backwards compatibility, so that the init argument order is preserved
uid = attr.ib(None)
@classmethod
def _from_graphql(cls, data):
return cls(
url=data.get("url"),
name=data.get("filename"),
is_malicious=data.get("is_malicious"),
uid=data.get("message_file_fbid"),
)
@attr.s(cmp=False)
class AudioAttachment(Attachment):
"""Represents an audio file that has been sent as a Facebook attachment."""
#: Name of the file
filename = attr.ib(None)
#: URL of the audio file
url = attr.ib(None)
#: Duration of the audio clip in milliseconds
duration = attr.ib(None)
#: Audio type
audio_type = attr.ib(None)
# Put here for backwards compatibility, so that the init argument order is preserved
uid = attr.ib(None)
@classmethod
def _from_graphql(cls, data):
return cls(
filename=data.get("filename"),
url=data.get("playable_url"),
duration=data.get("playable_duration_in_ms"),
audio_type=data.get("audio_type"),
)
@attr.s(cmp=False, init=False)
class ImageAttachment(Attachment):
"""Represents an image that has been sent as a Facebook attachment.
To retrieve the full image URL, use: `Client.fetchImageUrl`, and pass it the id of
the image attachment.
"""
#: The extension of the original image (e.g. ``png``)
original_extension = attr.ib(None)
#: Width of original image
width = attr.ib(None, converter=lambda x: None if x is None else int(x))
#: Height of original image
height = attr.ib(None, converter=lambda x: None if x is None else int(x))
#: Whether the image is animated
is_animated = attr.ib(None)
#: URL to a thumbnail of the image
thumbnail_url = attr.ib(None)
#: URL to a medium preview of the image
preview_url = attr.ib(None)
#: Width of the medium preview image
preview_width = attr.ib(None)
#: Height of the medium preview image
preview_height = attr.ib(None)
#: URL to a large preview of the image
large_preview_url = attr.ib(None)
#: Width of the large preview image
large_preview_width = attr.ib(None)
#: Height of the large preview image
large_preview_height = attr.ib(None)
#: URL to an animated preview of the image (e.g. for GIFs)
animated_preview_url = attr.ib(None)
#: Width of the animated preview image
animated_preview_width = attr.ib(None)
#: Height of the animated preview image
animated_preview_height = attr.ib(None)
def __init__(
self,
original_extension=None,
width=None,
height=None,
is_animated=None,
thumbnail_url=None,
preview=None,
large_preview=None,
animated_preview=None,
**kwargs
):
super(ImageAttachment, self).__init__(**kwargs)
self.original_extension = original_extension
if width is not None:
width = int(width)
self.width = width
if height is not None:
height = int(height)
self.height = height
self.is_animated = is_animated
self.thumbnail_url = thumbnail_url
if preview is None:
preview = {}
self.preview_url = preview.get("uri")
self.preview_width = preview.get("width")
self.preview_height = preview.get("height")
if large_preview is None:
large_preview = {}
self.large_preview_url = large_preview.get("uri")
self.large_preview_width = large_preview.get("width")
self.large_preview_height = large_preview.get("height")
if animated_preview is None:
animated_preview = {}
self.animated_preview_url = animated_preview.get("uri")
self.animated_preview_width = animated_preview.get("width")
self.animated_preview_height = animated_preview.get("height")
@classmethod
def _from_graphql(cls, data):
return cls(
original_extension=data.get("original_extension")
or (data["filename"].split("-")[0] if data.get("filename") else None),
width=data.get("original_dimensions", {}).get("width"),
height=data.get("original_dimensions", {}).get("height"),
is_animated=data["__typename"] == "MessageAnimatedImage",
thumbnail_url=data.get("thumbnail", {}).get("uri"),
preview=data.get("preview") or data.get("preview_image"),
large_preview=data.get("large_preview"),
animated_preview=data.get("animated_image"),
uid=data.get("legacy_attachment_id"),
)
@classmethod
def _from_list(cls, data):
data = data["node"]
return cls(
width=data["original_dimensions"].get("x"),
height=data["original_dimensions"].get("y"),
thumbnail_url=data["image"].get("uri"),
large_preview=data["image2"],
preview=data["image1"],
uid=data["legacy_attachment_id"],
)
@attr.s(cmp=False, init=False)
class VideoAttachment(Attachment):
"""Represents a video that has been sent as a Facebook attachment."""
#: Size of the original video in bytes
size = attr.ib(None)
#: Width of original video
width = attr.ib(None)
#: Height of original video
height = attr.ib(None)
#: Length of video in milliseconds
duration = attr.ib(None)
#: URL to very compressed preview video
preview_url = attr.ib(None)
#: URL to a small preview image of the video
small_image_url = attr.ib(None)
#: Width of the small preview image
small_image_width = attr.ib(None)
#: Height of the small preview image
small_image_height = attr.ib(None)
#: URL to a medium preview image of the video
medium_image_url = attr.ib(None)
#: Width of the medium preview image
medium_image_width = attr.ib(None)
#: Height of the medium preview image
medium_image_height = attr.ib(None)
#: URL to a large preview image of the video
large_image_url = attr.ib(None)
#: Width of the large preview image
large_image_width = attr.ib(None)
#: Height of the large preview image
large_image_height = attr.ib(None)
def __init__(
self,
size=None,
width=None,
height=None,
duration=None,
preview_url=None,
small_image=None,
medium_image=None,
large_image=None,
**kwargs
):
super(VideoAttachment, self).__init__(**kwargs)
self.size = size
self.width = width
self.height = height
self.duration = duration
self.preview_url = preview_url
if small_image is None:
small_image = {}
self.small_image_url = small_image.get("uri")
self.small_image_width = small_image.get("width")
self.small_image_height = small_image.get("height")
if medium_image is None:
medium_image = {}
self.medium_image_url = medium_image.get("uri")
self.medium_image_width = medium_image.get("width")
self.medium_image_height = medium_image.get("height")
if large_image is None:
large_image = {}
self.large_image_url = large_image.get("uri")
self.large_image_width = large_image.get("width")
self.large_image_height = large_image.get("height")
@classmethod
def _from_graphql(cls, data):
return cls(
width=data.get("original_dimensions", {}).get("width"),
height=data.get("original_dimensions", {}).get("height"),
duration=data.get("playable_duration_in_ms"),
preview_url=data.get("playable_url"),
small_image=data.get("chat_image"),
medium_image=data.get("inbox_image"),
large_image=data.get("large_image"),
uid=data.get("legacy_attachment_id"),
)
@classmethod
def _from_subattachment(cls, data):
media = data["media"]
return cls(
duration=media.get("playable_duration_in_ms"),
preview_url=media.get("playable_url"),
medium_image=media.get("image"),
uid=data["target"].get("video_id"),
)
@classmethod
def _from_list(cls, data):
data = data["node"]
return cls(
width=data["original_dimensions"].get("x"),
height=data["original_dimensions"].get("y"),
small_image=data["image"],
medium_image=data["image1"],
large_image=data["image2"],
uid=data["legacy_attachment_id"],
)
def graphql_to_attachment(data):
_type = data["__typename"]
if _type in ["MessageImage", "MessageAnimatedImage"]:
return ImageAttachment._from_graphql(data)
elif _type == "MessageVideo":
return VideoAttachment._from_graphql(data)
elif _type == "MessageAudio":
return AudioAttachment._from_graphql(data)
elif _type == "MessageFile":
return FileAttachment._from_graphql(data)
return Attachment(uid=data.get("legacy_attachment_id"))
def graphql_to_subattachment(data):
target = data.get("target")
type_ = target.get("__typename") if target else None
if type_ == "Video":
return VideoAttachment._from_subattachment(data)
return None

237
fbchat/_graphql.py Normal file
View File

@@ -0,0 +1,237 @@
# -*- coding: UTF-8 -*-
from __future__ import unicode_literals
import json
import re
from . import _util
from ._exception import FBchatException
# 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 queries_to_json(*queries):
"""
Queries should be a list of GraphQL objects
"""
rtn = {}
for i, query in enumerate(queries):
rtn["q{}".format(i)] = query
return json.dumps(rtn)
def response_to_json(content):
content = _util.strip_json_cruft(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
_util.handle_payload_error(x)
[(key, value)] = x.items()
_util.handle_graphql_errors(value)
if "response" in value:
rtn[int(key[1:])] = value["response"]
else:
rtn[int(key[1:])] = value["data"]
_util.log.debug(rtn)
return rtn
def from_query(query, params):
return {"priority": 0, "q": query, "query_params": params}
def from_query_id(query_id, params):
return {"query_id": query_id, "query_params": params}
def from_doc(doc, params):
return {"doc": doc, "query_params": params}
def from_doc_id(doc_id, params):
return {"doc_id": doc_id, "query_params": params}
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
)

118
fbchat/_group.py Normal file
View File

@@ -0,0 +1,118 @@
# -*- coding: UTF-8 -*-
from __future__ import unicode_literals
import attr
from . import _plan
from ._thread import ThreadType, Thread
@attr.s(cmp=False, init=False)
class Group(Thread):
"""Represents a Facebook group. Inherits `Thread`."""
#: Unique list (set) of the group thread's participant user IDs
participants = attr.ib(factory=set, converter=lambda x: set() if x is None else x)
#: A dictionary, containing user nicknames mapped to their IDs
nicknames = attr.ib(factory=dict, converter=lambda x: {} if x is None else x)
#: A :class:`ThreadColor`. The groups's message color
color = attr.ib(None)
#: The groups's default emoji
emoji = attr.ib(None)
# Set containing user IDs of thread admins
admins = attr.ib(factory=set, converter=lambda x: set() if x is None else x)
# True if users need approval to join
approval_mode = attr.ib(None)
# Set containing user IDs requesting to join
approval_requests = attr.ib(
factory=set, converter=lambda x: set() if x is None else x
)
# Link for joining group
join_link = attr.ib(None)
def __init__(
self,
uid,
participants=None,
nicknames=None,
color=None,
emoji=None,
admins=None,
approval_mode=None,
approval_requests=None,
join_link=None,
privacy_mode=None,
**kwargs
):
super(Group, self).__init__(ThreadType.GROUP, uid, **kwargs)
if participants is None:
participants = set()
self.participants = participants
if nicknames is None:
nicknames = []
self.nicknames = nicknames
self.color = color
self.emoji = emoji
if admins is None:
admins = set()
self.admins = admins
self.approval_mode = approval_mode
if approval_requests is None:
approval_requests = set()
self.approval_requests = approval_requests
self.join_link = join_link
@classmethod
def _from_graphql(cls, data):
if data.get("image") is None:
data["image"] = {}
c_info = cls._parse_customization_info(data)
last_message_timestamp = None
if "last_message" in data:
last_message_timestamp = data["last_message"]["nodes"][0][
"timestamp_precise"
]
plan = None
if data.get("event_reminders") and data["event_reminders"].get("nodes"):
plan = _plan.Plan._from_graphql(data["event_reminders"]["nodes"][0])
return cls(
data["thread_key"]["thread_fbid"],
participants=set(
[
node["messaging_actor"]["id"]
for node in data["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 data.get("thread_admins")]),
approval_mode=bool(data.get("approval_mode"))
if data.get("approval_mode") is not None
else None,
approval_requests=set(
node["requester"]["id"]
for node in data["group_approval_queue"]["nodes"]
)
if data.get("group_approval_queue")
else None,
join_link=data["joinable_mode"].get("link"),
photo=data["image"].get("uri"),
name=data.get("name"),
message_count=data.get("messages_count"),
last_message_timestamp=last_message_timestamp,
plan=plan,
)
@attr.s(cmp=False, init=False)
class Room(Group):
"""Deprecated. Use `Group` instead."""
# True is room is not discoverable
privacy_mode = attr.ib(None)
def __init__(self, uid, privacy_mode=None, **kwargs):
super(Room, self).__init__(uid, **kwargs)
self.type = ThreadType.ROOM
self.privacy_mode = privacy_mode

112
fbchat/_location.py Normal file
View File

@@ -0,0 +1,112 @@
# -*- coding: UTF-8 -*-
from __future__ import unicode_literals
import attr
from ._attachment import Attachment
from . import _util
@attr.s(cmp=False)
class LocationAttachment(Attachment):
"""Represents a user location.
Latitude and longitude OR address is provided by Facebook.
"""
#: Latitude of the location
latitude = attr.ib(None)
#: Longitude of the location
longitude = attr.ib(None)
#: URL of image showing the map of the location
image_url = attr.ib(None, init=False)
#: Width of the image
image_width = attr.ib(None, init=False)
#: Height of the image
image_height = attr.ib(None, init=False)
#: URL to Bing maps with the location
url = attr.ib(None, init=False)
# Address of the location
address = attr.ib(None)
# Put here for backwards compatibility, so that the init argument order is preserved
uid = attr.ib(None)
@classmethod
def _from_graphql(cls, data):
url = data.get("url")
address = _util.get_url_parameter(_util.get_url_parameter(url, "u"), "where1")
try:
latitude, longitude = [float(x) for x in address.split(", ")]
address = None
except ValueError:
latitude, longitude = None, None
rtn = cls(
uid=int(data["deduplication_key"]),
latitude=latitude,
longitude=longitude,
address=address,
)
media = data.get("media")
if media and media.get("image"):
image = media["image"]
rtn.image_url = image.get("uri")
rtn.image_width = image.get("width")
rtn.image_height = image.get("height")
rtn.url = url
return rtn
@attr.s(cmp=False, init=False)
class LiveLocationAttachment(LocationAttachment):
"""Represents a live user location."""
#: Name of the location
name = attr.ib(None)
#: Timestamp when live location expires
expiration_time = attr.ib(None)
#: True if live location is expired
is_expired = attr.ib(None)
def __init__(self, name=None, expiration_time=None, is_expired=None, **kwargs):
super(LiveLocationAttachment, self).__init__(**kwargs)
self.expiration_time = expiration_time
self.is_expired = is_expired
@classmethod
def _from_pull(cls, data):
return cls(
uid=data["id"],
latitude=data["coordinate"]["latitude"] / (10 ** 8)
if not data.get("stopReason")
else None,
longitude=data["coordinate"]["longitude"] / (10 ** 8)
if not data.get("stopReason")
else None,
name=data.get("locationTitle"),
expiration_time=data["expirationTime"],
is_expired=bool(data.get("stopReason")),
)
@classmethod
def _from_graphql(cls, data):
target = data["target"]
rtn = cls(
uid=int(target["live_location_id"]),
latitude=target["coordinate"]["latitude"]
if target.get("coordinate")
else None,
longitude=target["coordinate"]["longitude"]
if target.get("coordinate")
else None,
name=data["title_with_entities"]["text"],
expiration_time=target.get("expiration_time"),
is_expired=target.get("is_expired"),
)
media = data.get("media")
if media and media.get("image"):
image = media["image"]
rtn.image_url = image.get("uri")
rtn.image_width = image.get("width")
rtn.image_height = image.get("height")
rtn.url = data.get("url")
return rtn

346
fbchat/_message.py Normal file
View File

@@ -0,0 +1,346 @@
# -*- coding: UTF-8 -*-
from __future__ import unicode_literals
import attr
import json
from string import Formatter
from . import _util, _attachment, _location, _file, _quick_reply, _sticker
from ._core import Enum
class EmojiSize(Enum):
"""Used to specify the size of a sent emoji."""
LARGE = "369239383222810"
MEDIUM = "369239343222814"
SMALL = "369239263222822"
@classmethod
def _from_tags(cls, tags):
string_to_emojisize = {
"large": cls.LARGE,
"medium": cls.MEDIUM,
"small": cls.SMALL,
"l": cls.LARGE,
"m": cls.MEDIUM,
"s": cls.SMALL,
}
for tag in tags or ():
data = tag.split(":", maxsplit=1)
if len(data) > 1 and data[0] == "hot_emoji_size":
return string_to_emojisize.get(data[1])
return None
class MessageReaction(Enum):
"""Used to specify a message reaction."""
HEART = ""
LOVE = "😍"
SMILE = "😆"
WOW = "😮"
SAD = "😢"
ANGRY = "😠"
YES = "👍"
NO = "👎"
@attr.s(cmp=False)
class Mention(object):
"""Represents a ``@mention``."""
#: The thread ID the mention is pointing at
thread_id = attr.ib()
#: The character where the mention starts
offset = attr.ib(0)
#: The length of the mention
length = attr.ib(10)
@attr.s(cmp=False)
class Message(object):
"""Represents a Facebook message."""
#: The actual message
text = attr.ib(None)
#: A list of :class:`Mention` objects
mentions = attr.ib(factory=list, converter=lambda x: [] if x is None else x)
#: A :class:`EmojiSize`. Size of a sent emoji
emoji_size = attr.ib(None)
#: The message ID
uid = attr.ib(None, init=False)
#: ID of the sender
author = attr.ib(None, init=False)
#: Timestamp of when the message was sent
timestamp = attr.ib(None, init=False)
#: Whether the message is read
is_read = attr.ib(None, init=False)
#: A list of people IDs who read the message, works only with :func:`fbchat.Client.fetchThreadMessages`
read_by = attr.ib(factory=list, init=False)
#: A dictionary with user's IDs as keys, and their :class:`MessageReaction` as values
reactions = attr.ib(factory=dict, init=False)
#: A :class:`Sticker`
sticker = attr.ib(None)
#: A list of attachments
attachments = attr.ib(factory=list, converter=lambda x: [] if x is None else x)
#: A list of :class:`QuickReply`
quick_replies = attr.ib(factory=list, converter=lambda x: [] if x is None else x)
#: Whether the message is unsent (deleted for everyone)
unsent = attr.ib(False, init=False)
#: Message ID you want to reply to
reply_to_id = attr.ib(None)
#: Replied message
replied_to = attr.ib(None, init=False)
#: Whether the message was forwarded
forwarded = attr.ib(False, init=False)
@classmethod
def formatMentions(cls, text, *args, **kwargs):
"""Like `str.format`, but takes tuples with a thread id and text instead.
Return a `Message` object, with the formatted string and relevant mentions.
>>> Message.formatMentions("Hey {!r}! My name is {}", ("1234", "Peter"), ("4321", "Michael"))
<Message (None): "Hey 'Peter'! My name is Michael", mentions=[<Mention 1234: offset=4 length=7>, <Mention 4321: offset=24 length=7>] emoji_size=None attachments=[]>
>>> Message.formatMentions("Hey {p}! My name is {}", ("1234", "Michael"), p=("4321", "Peter"))
<Message (None): 'Hey Peter! My name is Michael', mentions=[<Mention 4321: offset=4 length=5>, <Mention 1234: offset=22 length=7>] emoji_size=None attachments=[]>
"""
result = ""
mentions = list()
offset = 0
f = Formatter()
field_names = [field_name[1] for field_name in f.parse(text)]
automatic = "" in field_names
i = 0
for (literal_text, field_name, format_spec, conversion) in f.parse(text):
offset += len(literal_text)
result += literal_text
if field_name is None:
continue
if field_name == "":
field_name = str(i)
i += 1
elif automatic and field_name.isdigit():
raise ValueError(
"cannot switch from automatic field numbering to manual field specification"
)
thread_id, name = f.get_field(field_name, args, kwargs)[0]
if format_spec:
name = f.format_field(name, format_spec)
if conversion:
name = f.convert_field(name, conversion)
result += name
mentions.append(
Mention(thread_id=thread_id, offset=offset, length=len(name))
)
offset += len(name)
message = cls(text=result, mentions=mentions)
return message
@staticmethod
def _get_forwarded_from_tags(tags):
if tags is None:
return False
return any(map(lambda tag: "forward" in tag or "copy" in tag, tags))
@classmethod
def _from_graphql(cls, data):
if data.get("message_sender") is None:
data["message_sender"] = {}
if data.get("message") is None:
data["message"] = {}
tags = data.get("tags_list")
rtn = cls(
text=data["message"].get("text"),
mentions=[
Mention(
m.get("entity", {}).get("id"),
offset=m.get("offset"),
length=m.get("length"),
)
for m in data["message"].get("ranges") or ()
],
emoji_size=EmojiSize._from_tags(tags),
sticker=_sticker.Sticker._from_graphql(data.get("sticker")),
)
rtn.forwarded = cls._get_forwarded_from_tags(tags)
rtn.uid = str(data["message_id"])
rtn.author = str(data["message_sender"]["id"])
rtn.timestamp = data.get("timestamp_precise")
rtn.unsent = False
if data.get("unread") is not None:
rtn.is_read = not data["unread"]
rtn.reactions = {
str(r["user"]["id"]): MessageReaction._extend_if_invalid(r["reaction"])
for r in data["message_reactions"]
}
if data.get("blob_attachments") is not None:
rtn.attachments = [
_file.graphql_to_attachment(attachment)
for attachment in data["blob_attachments"]
]
if data.get("platform_xmd_encoded"):
quick_replies = json.loads(data["platform_xmd_encoded"]).get(
"quick_replies"
)
if isinstance(quick_replies, list):
rtn.quick_replies = [
_quick_reply.graphql_to_quick_reply(q) for q in quick_replies
]
elif isinstance(quick_replies, dict):
rtn.quick_replies = [
_quick_reply.graphql_to_quick_reply(quick_replies, is_response=True)
]
if data.get("extensible_attachment") is not None:
attachment = graphql_to_extensible_attachment(data["extensible_attachment"])
if isinstance(attachment, _attachment.UnsentMessage):
rtn.unsent = True
elif attachment:
rtn.attachments.append(attachment)
if data.get("replied_to_message") is not None:
rtn.replied_to = cls._from_graphql(data["replied_to_message"]["message"])
rtn.reply_to_id = rtn.replied_to.uid
return rtn
@classmethod
def _from_reply(cls, data):
tags = data["messageMetadata"].get("tags")
rtn = cls(
text=data.get("body"),
mentions=[
Mention(m.get("i"), offset=m.get("o"), length=m.get("l"))
for m in json.loads(data.get("data", {}).get("prng", "[]"))
],
emoji_size=EmojiSize._from_tags(tags),
)
metadata = data.get("messageMetadata", {})
rtn.forwarded = cls._get_forwarded_from_tags(tags)
rtn.uid = metadata.get("messageId")
rtn.author = str(metadata.get("actorFbId"))
rtn.timestamp = metadata.get("timestamp")
rtn.unsent = False
if data.get("data", {}).get("platform_xmd"):
quick_replies = json.loads(data["data"]["platform_xmd"]).get(
"quick_replies"
)
if isinstance(quick_replies, list):
rtn.quick_replies = [
_quick_reply.graphql_to_quick_reply(q) for q in quick_replies
]
elif isinstance(quick_replies, dict):
rtn.quick_replies = [
_quick_reply.graphql_to_quick_reply(quick_replies, is_response=True)
]
if data.get("attachments") is not None:
for attachment in data["attachments"]:
attachment = json.loads(attachment["mercuryJSON"])
if attachment.get("blob_attachment"):
rtn.attachments.append(
_file.graphql_to_attachment(attachment["blob_attachment"])
)
if attachment.get("extensible_attachment"):
extensible_attachment = graphql_to_extensible_attachment(
attachment["extensible_attachment"]
)
if isinstance(extensible_attachment, _attachment.UnsentMessage):
rtn.unsent = True
else:
rtn.attachments.append(extensible_attachment)
if attachment.get("sticker_attachment"):
rtn.sticker = _sticker.Sticker._from_graphql(
attachment["sticker_attachment"]
)
return rtn
@classmethod
def _from_pull(cls, data, mid=None, tags=None, author=None, timestamp=None):
rtn = cls(text=data.get("body"))
rtn.uid = mid
rtn.author = author
rtn.timestamp = timestamp
if data.get("data") and data["data"].get("prng"):
try:
rtn.mentions = [
Mention(
str(mention.get("i")),
offset=mention.get("o"),
length=mention.get("l"),
)
for mention in _util.parse_json(data["data"]["prng"])
]
except Exception:
_util.log.exception("An exception occured while reading attachments")
if data.get("attachments"):
try:
for a in data["attachments"]:
mercury = a["mercury"]
if mercury.get("blob_attachment"):
image_metadata = a.get("imageMetadata", {})
attach_type = mercury["blob_attachment"]["__typename"]
attachment = _file.graphql_to_attachment(
mercury["blob_attachment"]
)
if attach_type in [
"MessageFile",
"MessageVideo",
"MessageAudio",
]:
# TODO: Add more data here for audio files
attachment.size = int(a["fileSize"])
rtn.attachments.append(attachment)
elif mercury.get("sticker_attachment"):
rtn.sticker = _sticker.Sticker._from_graphql(
mercury["sticker_attachment"]
)
elif mercury.get("extensible_attachment"):
attachment = graphql_to_extensible_attachment(
mercury["extensible_attachment"]
)
if isinstance(attachment, _attachment.UnsentMessage):
rtn.unsent = True
elif attachment:
rtn.attachments.append(attachment)
except Exception:
_util.log.exception(
"An exception occured while reading attachments: {}".format(
data["attachments"]
)
)
rtn.emoji_size = EmojiSize._from_tags(tags)
rtn.forwarded = cls._get_forwarded_from_tags(tags)
return rtn
def graphql_to_extensible_attachment(data):
story = data.get("story_attachment")
if not story:
return None
target = story.get("target")
if not target:
return _attachment.UnsentMessage(uid=data.get("legacy_attachment_id"))
_type = target["__typename"]
if _type == "MessageLocation":
return _location.LocationAttachment._from_graphql(story)
elif _type == "MessageLiveLocation":
return _location.LiveLocationAttachment._from_graphql(story)
elif _type in ["ExternalUrl", "Story"]:
return _attachment.ShareAttachment._from_graphql(story)
return None

60
fbchat/_page.py Normal file
View File

@@ -0,0 +1,60 @@
# -*- coding: UTF-8 -*-
from __future__ import unicode_literals
import attr
from . import _plan
from ._thread import ThreadType, Thread
@attr.s(cmp=False, init=False)
class Page(Thread):
"""Represents a Facebook page. Inherits `Thread`."""
#: The page's custom URL
url = attr.ib(None)
#: The name of the page's location city
city = attr.ib(None)
#: Amount of likes the page has
likes = attr.ib(None)
#: Some extra information about the page
sub_title = attr.ib(None)
#: The page's category
category = attr.ib(None)
def __init__(
self,
uid,
url=None,
city=None,
likes=None,
sub_title=None,
category=None,
**kwargs
):
super(Page, self).__init__(ThreadType.PAGE, uid, **kwargs)
self.url = url
self.city = city
self.likes = likes
self.sub_title = sub_title
self.category = category
@classmethod
def _from_graphql(cls, data):
if data.get("profile_picture") is None:
data["profile_picture"] = {}
if data.get("city") is None:
data["city"] = {}
plan = None
if data.get("event_reminders") and data["event_reminders"].get("nodes"):
plan = _plan.Plan._from_graphql(data["event_reminders"]["nodes"][0])
return cls(
data["id"],
url=data.get("url"),
city=data.get("city").get("name"),
category=data.get("category_type"),
photo=data["profile_picture"].get("uri"),
name=data.get("name"),
message_count=data.get("messages_count"),
plan=plan,
)

103
fbchat/_plan.py Normal file
View File

@@ -0,0 +1,103 @@
# -*- coding: UTF-8 -*-
from __future__ import unicode_literals
import attr
import json
from ._core import Enum
class GuestStatus(Enum):
INVITED = 1
GOING = 2
DECLINED = 3
@attr.s(cmp=False)
class Plan(object):
"""Represents a plan."""
#: ID of the plan
uid = attr.ib(None, init=False)
#: Plan time (timestamp), only precise down to the minute
time = attr.ib(converter=int)
#: Plan title
title = attr.ib()
#: Plan location name
location = attr.ib(None, converter=lambda x: x or "")
#: Plan location ID
location_id = attr.ib(None, converter=lambda x: x or "")
#: ID of the plan creator
author_id = attr.ib(None, init=False)
#: Dictionary of `User` IDs mapped to their `GuestStatus`
guests = attr.ib(None, init=False)
@property
def going(self):
"""List of the `User` IDs who will take part in the plan."""
return [
id_
for id_, status in (self.guests or {}).items()
if status is GuestStatus.GOING
]
@property
def declined(self):
"""List of the `User` IDs who won't take part in the plan."""
return [
id_
for id_, status in (self.guests or {}).items()
if status is GuestStatus.DECLINED
]
@property
def invited(self):
"""List of the `User` IDs who are invited to the plan."""
return [
id_
for id_, status in (self.guests or {}).items()
if status is GuestStatus.INVITED
]
@classmethod
def _from_pull(cls, data):
rtn = cls(
time=data.get("event_time"),
title=data.get("event_title"),
location=data.get("event_location_name"),
location_id=data.get("event_location_id"),
)
rtn.uid = data.get("event_id")
rtn.author_id = data.get("event_creator_id")
rtn.guests = {
x["node"]["id"]: GuestStatus[x["guest_list_state"]]
for x in json.loads(data["guest_state_list"])
}
return rtn
@classmethod
def _from_fetch(cls, data):
rtn = cls(
time=data.get("event_time"),
title=data.get("title"),
location=data.get("location_name"),
location_id=str(data["location_id"]) if data.get("location_id") else None,
)
rtn.uid = data.get("oid")
rtn.author_id = data.get("creator_id")
rtn.guests = {id_: GuestStatus[s] for id_, s in data["event_members"].items()}
return rtn
@classmethod
def _from_graphql(cls, data):
rtn = cls(
time=data.get("time"),
title=data.get("event_title"),
location=data.get("location_name"),
)
rtn.uid = data.get("id")
rtn.author_id = data["lightweight_event_creator"].get("id")
rtn.guests = {
x["node"]["id"]: GuestStatus[x["guest_list_state"]]
for x in data["event_reminder_members"]["edges"]
}
return rtn

67
fbchat/_poll.py Normal file
View File

@@ -0,0 +1,67 @@
# -*- coding: UTF-8 -*-
from __future__ import unicode_literals
import attr
@attr.s(cmp=False)
class Poll(object):
"""Represents a poll."""
#: Title of the poll
title = attr.ib()
#: List of :class:`PollOption`, can be fetched with :func:`fbchat.Client.fetchPollOptions`
options = attr.ib()
#: Options count
options_count = attr.ib(None)
#: ID of the poll
uid = attr.ib(None)
@classmethod
def _from_graphql(cls, data):
return cls(
uid=int(data["id"]),
title=data.get("title") if data.get("title") else data.get("text"),
options=[PollOption._from_graphql(m) for m in data.get("options")],
options_count=data.get("total_count"),
)
@attr.s(cmp=False)
class PollOption(object):
"""Represents a poll option."""
#: Text of the poll option
text = attr.ib()
#: Whether vote when creating or client voted
vote = attr.ib(False)
#: ID of the users who voted for this poll option
voters = attr.ib(None)
#: Votes count
votes_count = attr.ib(None)
#: ID of the poll option
uid = attr.ib(None)
@classmethod
def _from_graphql(cls, data):
if data.get("viewer_has_voted") is None:
vote = None
elif isinstance(data["viewer_has_voted"], bool):
vote = data["viewer_has_voted"]
else:
vote = data["viewer_has_voted"] == "true"
return cls(
uid=int(data["id"]),
text=data.get("text"),
vote=vote,
voters=(
[m.get("node").get("id") for m in data.get("voters").get("edges")]
if isinstance(data.get("voters"), dict)
else data.get("voters")
),
votes_count=(
data.get("voters").get("count")
if isinstance(data.get("voters"), dict)
else data.get("total_count")
),
)

99
fbchat/_quick_reply.py Normal file
View File

@@ -0,0 +1,99 @@
# -*- coding: UTF-8 -*-
from __future__ import unicode_literals
import attr
from ._attachment import Attachment
@attr.s(cmp=False)
class QuickReply(object):
"""Represents a quick reply."""
#: Payload of the quick reply
payload = attr.ib(None)
#: External payload for responses
external_payload = attr.ib(None, init=False)
#: Additional data
data = attr.ib(None)
#: Whether it's a response for a quick reply
is_response = attr.ib(False)
@attr.s(cmp=False, init=False)
class QuickReplyText(QuickReply):
"""Represents a text quick reply."""
#: Title of the quick reply
title = attr.ib(None)
#: URL of the quick reply image (optional)
image_url = attr.ib(None)
#: Type of the quick reply
_type = "text"
def __init__(self, title=None, image_url=None, **kwargs):
super(QuickReplyText, self).__init__(**kwargs)
self.title = title
self.image_url = image_url
@attr.s(cmp=False, init=False)
class QuickReplyLocation(QuickReply):
"""Represents a location quick reply (Doesn't work on mobile)."""
#: Type of the quick reply
_type = "location"
def __init__(self, **kwargs):
super(QuickReplyLocation, self).__init__(**kwargs)
self.is_response = False
@attr.s(cmp=False, init=False)
class QuickReplyPhoneNumber(QuickReply):
"""Represents a phone number quick reply (Doesn't work on mobile)."""
#: URL of the quick reply image (optional)
image_url = attr.ib(None)
#: Type of the quick reply
_type = "user_phone_number"
def __init__(self, image_url=None, **kwargs):
super(QuickReplyPhoneNumber, self).__init__(**kwargs)
self.image_url = image_url
@attr.s(cmp=False, init=False)
class QuickReplyEmail(QuickReply):
"""Represents an email quick reply (Doesn't work on mobile)."""
#: URL of the quick reply image (optional)
image_url = attr.ib(None)
#: Type of the quick reply
_type = "user_email"
def __init__(self, image_url=None, **kwargs):
super(QuickReplyEmail, self).__init__(**kwargs)
self.image_url = image_url
def graphql_to_quick_reply(q, is_response=False):
data = dict()
_type = q.get("content_type").lower()
if q.get("payload"):
data["payload"] = q["payload"]
if q.get("data"):
data["data"] = q["data"]
if q.get("image_url") and _type is not QuickReplyLocation._type:
data["image_url"] = q["image_url"]
data["is_response"] = is_response
if _type == QuickReplyText._type:
if q.get("title") is not None:
data["title"] = q["title"]
rtn = QuickReplyText(**data)
elif _type == QuickReplyLocation._type:
rtn = QuickReplyLocation(**data)
elif _type == QuickReplyPhoneNumber._type:
rtn = QuickReplyPhoneNumber(**data)
elif _type == QuickReplyEmail._type:
rtn = QuickReplyEmail(**data)
return rtn

193
fbchat/_state.py Normal file
View File

@@ -0,0 +1,193 @@
# -*- coding: UTF-8 -*-
from __future__ import unicode_literals
import attr
import bs4
import re
import requests
import random
from . import _util, _exception
FB_DTSG_REGEX = re.compile(r'name="fb_dtsg" value="(.*?)"')
def find_input_fields(html):
return bs4.BeautifulSoup(html, "html.parser", parse_only=bs4.SoupStrainer("input"))
def session_factory(user_agent=None):
session = requests.session()
session.headers["Referer"] = "https://www.facebook.com"
# TODO: Deprecate setting the user agent manually
session.headers["User-Agent"] = user_agent or random.choice(_util.USER_AGENTS)
return session
def is_home(url):
parts = _util.urlparse(url)
# Check the urls `/home.php` and `/`
return "home" in parts.path or "/" == parts.path
def _2fa_helper(session, code, r):
soup = find_input_fields(r.text)
data = dict()
url = "https://m.facebook.com/login/checkpoint/"
data["approvals_code"] = code
data["fb_dtsg"] = soup.find("input", {"name": "fb_dtsg"})["value"]
data["nh"] = soup.find("input", {"name": "nh"})["value"]
data["submit[Submit Code]"] = "Submit Code"
data["codes_submitted"] = 0
_util.log.info("Submitting 2FA code.")
r = session.post(url, data=data)
if is_home(r.url):
return r
del data["approvals_code"]
del data["submit[Submit Code]"]
del data["codes_submitted"]
data["name_action_selected"] = "save_device"
data["submit[Continue]"] = "Continue"
_util.log.info("Saving browser.")
# At this stage, we have dtsg, nh, name_action_selected, submit[Continue]
r = session.post(url, data=data)
if is_home(r.url):
return r
del data["name_action_selected"]
_util.log.info("Starting Facebook checkup flow.")
# At this stage, we have dtsg, nh, submit[Continue]
r = session.post(url, data=data)
if is_home(r.url):
return r
del data["submit[Continue]"]
data["submit[This was me]"] = "This Was Me"
_util.log.info("Verifying login attempt.")
# At this stage, we have dtsg, nh, submit[This was me]
r = session.post(url, data=data)
if is_home(r.url):
return r
del data["submit[This was me]"]
data["submit[Continue]"] = "Continue"
data["name_action_selected"] = "save_device"
_util.log.info("Saving device again.")
# At this stage, we have dtsg, nh, submit[Continue], name_action_selected
r = session.post(url, data=data)
return r
@attr.s(slots=True) # TODO i Python 3: Add kw_only=True
class State(object):
"""Stores and manages state required for most Facebook requests."""
fb_dtsg = attr.ib()
_revision = attr.ib()
_session = attr.ib(factory=session_factory)
_counter = attr.ib(0)
_logout_h = attr.ib(None)
def get_user_id(self):
rtn = self.get_cookies().get("c_user")
if rtn is None:
return None
return str(rtn)
def get_params(self):
self._counter += 1 # TODO: Make this operation atomic / thread-safe
return {
"__a": 1,
"__req": _util.str_base(self._counter, 36),
"__rev": self._revision,
"fb_dtsg": self.fb_dtsg,
}
@classmethod
def login(cls, email, password, on_2fa_callback, user_agent=None):
session = session_factory(user_agent=user_agent)
soup = find_input_fields(session.get("https://m.facebook.com/").text)
data = dict(
(elem["name"], elem["value"])
for elem in soup
if elem.has_attr("value") and elem.has_attr("name")
)
data["email"] = email
data["pass"] = password
data["login"] = "Log In"
r = session.post("https://m.facebook.com/login.php?login_attempt=1", data=data)
# Usually, 'Checkpoint' will refer to 2FA
if "checkpoint" in r.url and ('id="approvals_code"' in r.text.lower()):
code = on_2fa_callback()
r = _2fa_helper(session, code, r)
# Sometimes Facebook tries to show the user a "Save Device" dialog
if "save-device" in r.url:
r = session.get("https://m.facebook.com/login/save-device/cancel/")
if is_home(r.url):
return cls.from_session(session=session)
else:
raise _exception.FBchatUserError(
"Login failed. Check email/password. "
"(Failed on url: {})".format(r.url)
)
def is_logged_in(self):
# Send a request to the login url, to see if we're directed to the home page
url = "https://m.facebook.com/login.php?login_attempt=1"
r = self._session.get(url, allow_redirects=False)
return "Location" in r.headers and is_home(r.headers["Location"])
def logout(self):
logout_h = self._logout_h
if not logout_h:
url = _util.prefix_url("/bluebar/modern_settings_menu/")
h_r = self._session.post(url, data={"pmid": "4"})
logout_h = re.search(r'name=\\"h\\" value=\\"(.*?)\\"', h_r.text).group(1)
url = _util.prefix_url("/logout.php")
return self._session.get(url, params={"ref": "mb", "h": logout_h}).ok
@classmethod
def from_session(cls, session):
r = session.get(_util.prefix_url("/"))
soup = find_input_fields(r.text)
fb_dtsg_element = soup.find("input", {"name": "fb_dtsg"})
if fb_dtsg_element:
fb_dtsg = fb_dtsg_element["value"]
else:
# Fall back to searching with a regex
fb_dtsg = FB_DTSG_REGEX.search(r.text).group(1)
revision = int(r.text.split('"client_revision":', 1)[1].split(",", 1)[0])
logout_h_element = soup.find("input", {"name": "h"})
logout_h = logout_h_element["value"] if logout_h_element else None
return cls(
fb_dtsg=fb_dtsg, revision=revision, session=session, logout_h=logout_h
)
def get_cookies(self):
return self._session.cookies.get_dict()
@classmethod
def from_cookies(cls, cookies, user_agent=None):
session = session_factory(user_agent=user_agent)
session.cookies = requests.cookies.merge_cookies(session.cookies, cookies)
return cls.from_session(session=session)

60
fbchat/_sticker.py Normal file
View File

@@ -0,0 +1,60 @@
# -*- coding: UTF-8 -*-
from __future__ import unicode_literals
import attr
from ._attachment import Attachment
@attr.s(cmp=False, init=False)
class Sticker(Attachment):
"""Represents a Facebook sticker that has been sent to a thread as an attachment."""
#: The sticker-pack's ID
pack = attr.ib(None)
#: Whether the sticker is animated
is_animated = attr.ib(False)
# If the sticker is animated, the following should be present
#: URL to a medium spritemap
medium_sprite_image = attr.ib(None)
#: URL to a large spritemap
large_sprite_image = attr.ib(None)
#: The amount of frames present in the spritemap pr. row
frames_per_row = attr.ib(None)
#: The amount of frames present in the spritemap pr. column
frames_per_col = attr.ib(None)
#: The frame rate the spritemap is intended to be played in
frame_rate = attr.ib(None)
#: URL to the sticker's image
url = attr.ib(None)
#: Width of the sticker
width = attr.ib(None)
#: Height of the sticker
height = attr.ib(None)
#: The sticker's label/name
label = attr.ib(None)
def __init__(self, uid=None):
super(Sticker, self).__init__(uid=uid)
@classmethod
def _from_graphql(cls, data):
if not data:
return None
self = cls(uid=data["id"])
if data.get("pack"):
self.pack = data["pack"].get("id")
if data.get("sprite_image"):
self.is_animated = True
self.medium_sprite_image = data["sprite_image"].get("uri")
self.large_sprite_image = data["sprite_image_2x"].get("uri")
self.frames_per_row = data.get("frames_per_row")
self.frames_per_col = data.get("frames_per_column")
self.frame_rate = data.get("frame_rate")
self.url = data.get("url")
self.width = data.get("width")
self.height = data.get("height")
if data.get("label"):
self.label = data["label"]
return self

132
fbchat/_thread.py Normal file
View File

@@ -0,0 +1,132 @@
# -*- coding: UTF-8 -*-
from __future__ import unicode_literals
import attr
from ._core import Enum
class ThreadType(Enum):
"""Used to specify what type of Facebook thread is being used.
See :ref:`intro_threads` for more info.
"""
USER = 1
GROUP = 2
ROOM = 2
PAGE = 3
class ThreadLocation(Enum):
"""Used to specify where a thread is located (inbox, pending, archived, other)."""
INBOX = "INBOX"
PENDING = "PENDING"
ARCHIVED = "ARCHIVED"
OTHER = "OTHER"
class ThreadColor(Enum):
"""Used to specify a thread colors."""
MESSENGER_BLUE = "#0084ff"
VIKING = "#44bec7"
GOLDEN_POPPY = "#ffc300"
RADICAL_RED = "#fa3c4c"
SHOCKING = "#d696bb"
PICTON_BLUE = "#6699cc"
FREE_SPEECH_GREEN = "#13cf13"
PUMPKIN = "#ff7e29"
LIGHT_CORAL = "#e68585"
MEDIUM_SLATE_BLUE = "#7646ff"
DEEP_SKY_BLUE = "#20cef5"
FERN = "#67b868"
CAMEO = "#d4a88c"
BRILLIANT_ROSE = "#ff5ca1"
BILOBA_FLOWER = "#a695c7"
TICKLE_ME_PINK = "#ff7ca8"
MALACHITE = "#1adb5b"
RUBY = "#f01d6a"
DARK_TANGERINE = "#ff9c19"
BRIGHT_TURQUOISE = "#0edcde"
@classmethod
def _from_graphql(cls, color):
if color is None:
return None
if not color:
return cls.MESSENGER_BLUE
color = color[2:] # Strip the alpha value
value = "#{}".format(color.lower())
return cls._extend_if_invalid(value)
@attr.s(cmp=False, init=False)
class Thread(object):
"""Represents a Facebook thread."""
#: The unique identifier of the thread. Can be used a ``thread_id``. See :ref:`intro_threads` for more info
uid = attr.ib(converter=str)
#: Specifies the type of thread. Can be used a ``thread_type``. See :ref:`intro_threads` for more info
type = attr.ib()
#: A URL to the thread's picture
photo = attr.ib(None)
#: The name of the thread
name = attr.ib(None)
#: Timestamp of last message
last_message_timestamp = attr.ib(None)
#: Number of messages in the thread
message_count = attr.ib(None)
#: Set :class:`Plan`
plan = attr.ib(None)
def __init__(
self,
_type,
uid,
photo=None,
name=None,
last_message_timestamp=None,
message_count=None,
plan=None,
):
self.uid = str(uid)
self.type = _type
self.photo = photo
self.name = name
self.last_message_timestamp = last_message_timestamp
self.message_count = message_count
self.plan = plan
@staticmethod
def _parse_customization_info(data):
if data is None or data.get("customization_info") is None:
return {}
info = data["customization_info"]
rtn = {
"emoji": info.get("emoji"),
"color": ThreadColor._from_graphql(info.get("outgoing_bubble_color")),
}
if (
data.get("thread_type") == "GROUP"
or data.get("is_group_thread")
or data.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 = data.get("thread_key", {}).get("other_user_id") or data.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

208
fbchat/_user.py Normal file
View File

@@ -0,0 +1,208 @@
# -*- coding: UTF-8 -*-
from __future__ import unicode_literals
import attr
from ._core import Enum
from . import _plan
from ._thread import ThreadType, Thread
GENDERS = {
# For standard requests
0: "unknown",
1: "female_singular",
2: "male_singular",
3: "female_singular_guess",
4: "male_singular_guess",
5: "mixed",
6: "neuter_singular",
7: "unknown_singular",
8: "female_plural",
9: "male_plural",
10: "neuter_plural",
11: "unknown_plural",
# 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 TypingStatus(Enum):
"""Used to specify whether the user is typing or has stopped typing."""
STOPPED = 0
TYPING = 1
@attr.s(cmp=False, init=False)
class User(Thread):
"""Represents a Facebook user. Inherits `Thread`."""
#: The profile URL
url = attr.ib(None)
#: The users first name
first_name = attr.ib(None)
#: The users last name
last_name = attr.ib(None)
#: Whether the user and the client are friends
is_friend = attr.ib(None)
#: The user's gender
gender = attr.ib(None)
#: From 0 to 1. How close the client is to the user
affinity = attr.ib(None)
#: The user's nickname
nickname = attr.ib(None)
#: The clients nickname, as seen by the user
own_nickname = attr.ib(None)
#: A :class:`ThreadColor`. The message color
color = attr.ib(None)
#: The default emoji
emoji = attr.ib(None)
def __init__(
self,
uid,
url=None,
first_name=None,
last_name=None,
is_friend=None,
gender=None,
affinity=None,
nickname=None,
own_nickname=None,
color=None,
emoji=None,
**kwargs
):
super(User, self).__init__(ThreadType.USER, uid, **kwargs)
self.url = url
self.first_name = first_name
self.last_name = last_name
self.is_friend = is_friend
self.gender = gender
self.affinity = affinity
self.nickname = nickname
self.own_nickname = own_nickname
self.color = color
self.emoji = emoji
@classmethod
def _from_graphql(cls, data):
if data.get("profile_picture") is None:
data["profile_picture"] = {}
c_info = cls._parse_customization_info(data)
plan = None
if data.get("event_reminders") and data["event_reminders"].get("nodes"):
plan = _plan.Plan._from_graphql(data["event_reminders"]["nodes"][0])
return cls(
data["id"],
url=data.get("url"),
first_name=data.get("first_name"),
last_name=data.get("last_name"),
is_friend=data.get("is_viewer_friend"),
gender=GENDERS.get(data.get("gender")),
affinity=data.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=data["profile_picture"].get("uri"),
name=data.get("name"),
message_count=data.get("messages_count"),
plan=plan,
)
@classmethod
def _from_thread_fetch(cls, data):
if data.get("big_image_src") is None:
data["big_image_src"] = {}
c_info = cls._parse_customization_info(data)
participants = [
node["messaging_actor"] for node in data["all_participants"]["nodes"]
]
user = next(
p for p in participants if p["id"] == data["thread_key"]["other_user_id"]
)
last_message_timestamp = None
if "last_message" in data:
last_message_timestamp = data["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 data.get("event_reminders") and data["event_reminders"].get("nodes"):
plan = _plan.Plan._from_graphql(data["event_reminders"]["nodes"][0])
return cls(
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=data.get("messages_count"),
last_message_timestamp=last_message_timestamp,
plan=plan,
)
@classmethod
def _from_all_fetch(cls, data):
return cls(
data["id"],
first_name=data.get("firstName"),
url=data.get("uri"),
photo=data.get("thumbSrc"),
name=data.get("name"),
is_friend=data.get("is_friend"),
gender=GENDERS.get(data.get("gender")),
)
@attr.s(cmp=False)
class ActiveStatus(object):
#: Whether the user is active now
active = attr.ib(None)
#: Timestamp when the user was last active
last_active = attr.ib(None)
#: Whether the user is playing Messenger game now
in_game = attr.ib(None)
@classmethod
def _from_chatproxy_presence(cls, id_, data):
return cls(
active=data["p"] in [2, 3] if "p" in data else None,
last_active=data.get("lat"),
in_game=int(id_) in data.get("gamers", {}),
)
@classmethod
def _from_buddylist_overlay(cls, data, in_game=None):
return cls(
active=data["a"] in [2, 3] if "a" in data else None,
last_active=data.get("la"),
in_game=None,
)

256
fbchat/_util.py Normal file
View File

@@ -0,0 +1,256 @@
# -*- 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
from ._exception import (
FBchatException,
FBchatFacebookError,
FBchatInvalidParameters,
FBchatNotLoggedIn,
FBchatPleaseRefresh,
)
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",
"Mozilla/5.0 (Windows NT 6.3; WOW64; ; NCT50_AAP285C84A1328) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.90 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/22.0.1207.1 Safari/537.1",
"Mozilla/5.0 (X11; CrOS i686 2268.111.0) AppleWebKit/536.11 (KHTML, like Gecko) Chrome/20.0.1132.57 Safari/536.11",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.6 (KHTML, like Gecko) Chrome/20.0.1092.0 Safari/536.6",
]
def now():
return int(time() * 1000)
def strip_json_cruft(text):
"""Removes `for(;;);` (and other cruft) that preceeds JSON responses."""
try:
return text[text.index("{") :]
except ValueError:
raise FBchatException("No JSON object found: {!r}".format(text))
def get_decoded_r(r):
return get_decoded(r._content)
def get_decoded(content):
return content.decode("utf-8")
def parse_json(content):
try:
return json.loads(content)
except ValueError:
raise FBchatFacebookError("Error while parsing JSON: {!r}".format(content))
def digitToChar(digit):
if digit < 10:
return str(digit)
return chr(ord("a") + digit - 10)
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) + digitToChar(m)
return digitToChar(m)
def generateMessageID(client_id=None):
k = now()
l = int(random() * 4294967295)
return "<{}:{}-{}@mail.projektitan.com>".format(k, l, client_id)
def getSignatureID():
return hex(int(random() * 2147483648))
def generateOfflineThreadingID():
ret = now()
value = int(random() * 4294967295)
string = ("0000000000000000000000" + format(value, "b"))[-22:]
msgs = format(ret, "b") + string
return str(int(msgs, 2))
def handle_payload_error(j):
if "error" not in j:
return
error = j["error"]
if j["error"] == 1357001:
error_cls = FBchatNotLoggedIn
elif j["error"] == 1357004:
error_cls = FBchatPleaseRefresh
elif j["error"] in (1357031, 1545010, 1545003):
error_cls = FBchatInvalidParameters
else:
error_cls = FBchatFacebookError
# TODO: Use j["errorSummary"]
# "errorDescription" is in the users own language!
raise error_cls(
"Error #{} when sending request: {}".format(error, j["errorDescription"]),
fb_error_code=error,
fb_error_message=j["errorDescription"],
)
def handle_graphql_errors(j):
errors = []
if j.get("error"):
errors = [j["error"]]
if "errors" in j:
errors = j["errors"]
if errors:
error = errors[0] # TODO: Handle multiple errors
# TODO: Use `summary`, `severity` and `description`
raise FBchatFacebookError(
"GraphQL error #{}: {} / {!r}".format(
error.get("code"), error.get("message"), error.get("debug_info")
),
fb_error_code=error.get("code"),
fb_error_message=error.get("message"),
)
def check_request(r):
check_http_code(r.status_code)
content = get_decoded_r(r)
check_content(content)
return content
def check_http_code(code):
msg = "Error when sending request: Got {} response.".format(code)
if code == 404:
raise FBchatFacebookError(
msg + " This is either because you specified an invalid URL, or because"
" you provided an invalid id (Facebook usually requires integer ids).",
request_status_code=code,
)
if 400 <= code < 600:
raise FBchatFacebookError(msg, request_status_code=code)
def check_content(content, as_json=True):
if content is None or len(content) == 0:
raise FBchatFacebookError("Error when sending request: Got empty response")
def to_json(content):
content = strip_json_cruft(content)
j = parse_json(content)
log.debug(j)
return j
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 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).split("?")[0].split("#")[0],
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 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]
def prefix_url(url):
if url.startswith("/"):
return "https://www.facebook.com" + url
return url

File diff suppressed because it is too large Load Diff

View File

@@ -1,378 +0,0 @@
# -*- 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 len(color) == 0:
return ThreadColor.MESSENGER_BLUE
try:
return ThreadColor('#{}'.format(color[2:].lower()))
except ValueError:
raise FBchatException('Could not get ThreadColor from color: {}'.format(color))
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') in ('GROUP', 'ROOM') 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 == '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_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')
if message.get('unread') is not None:
rtn.is_read = not message['unread']
rtn.reactions = {str(r['user']['id']):MessageReaction(r['reaction']) for r in message.get('message_reactions')}
if message.get('blob_attachments') is not None:
rtn.attachments = [graphql_to_attachment(attachment) for attachment in message['blob_attachments']]
# TODO: This is still missing parsing:
# message.get('extensible_attachment')
return rtn
def graphql_to_user(user):
if user.get('profile_picture') is None:
user['profile_picture'] = {}
c_info = get_customization_info(user)
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')
)
def graphql_to_group(group):
if group.get('image') is None:
group['image'] = {}
c_info = get_customization_info(group)
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'),
photo=group['image'].get('uri'),
name=group.get('name'),
message_count=group.get('messages_count')
)
def graphql_to_room(room):
if room.get('image') is None:
room['image'] = {}
c_info = get_customization_info(room)
return Room(
room['thread_key']['thread_fbid'],
participants=set([node['messaging_actor']['id'] for node in room['all_participants']['nodes']]),
nicknames=c_info.get('nicknames'),
color=c_info.get('color'),
emoji=c_info.get('emoji'),
photo=room['image'].get('uri'),
name=room.get('name'),
message_count=room.get('messages_count'),
admins = set([node.get('id') for node in room.get('thread_admins')]),
approval_mode = bool(room.get('approval_mode')),
approval_requests = set(node.get('id') for node in room['thread_queue_metadata'].get('approval_requests', {}).get('nodes')),
join_link = room['joinable_mode'].get('link'),
privacy_mode = bool(room.get('privacy_mode')),
)
def graphql_to_page(page):
if page.get('profile_picture') is None:
page['profile_picture'] = {}
if page.get('city') is None:
page['city'] = {}
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')
)
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
}
}
"""
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> = 1) {
entities_named(<search>) {
search_results.of_type(user).first(<limit>) as users {
nodes {
@User
}
}
}
}
""" + FRAGMENT_USER
SEARCH_GROUP = """
Query SearchGroup(<search> = '', <limit> = 1, <pic_size> = 32) {
viewer() {
message_threads.with_thread_name(<search>).last(<limit>) as groups {
nodes {
@Group
}
}
}
}
""" + FRAGMENT_GROUP
SEARCH_PAGE = """
Query SearchPage(<search> = '', <limit> = 1) {
entities_named(<search>) {
search_results.of_type(page).first(<limit>) as pages {
nodes {
@Page
}
}
}
}
""" + FRAGMENT_PAGE
SEARCH_THREAD = """
Query SearchThread(<search> = '', <limit> = 1) {
entities_named(<search>) {
search_results.first(<limit>) as threads {
nodes {
__typename,
@User,
@Group,
@Page
}
}
}
}
""" + FRAGMENT_USER + FRAGMENT_GROUP + FRAGMENT_PAGE

View File

@@ -1,483 +1,29 @@
# -*- coding: UTF-8 -*-
"""This file is here to maintain backwards compatability, and to re-export our models
into the global module (see `__init__.py`).
A common pattern was to use `from fbchat.models import *`, hence we need this while
transitioning to a better code structure.
"""
from __future__ import unicode_literals
import enum
class FBchatException(Exception):
"""Custom exception thrown by fbchat. All exceptions in the fbchat module inherits this"""
class FBchatFacebookError(FBchatException):
#: The error code that Facebook returned
fb_error_code = str
#: The error message that Facebook returned (In the user's own language)
fb_error_message = str
#: The status code that was sent in the http response (eg. 404) (Usually only set if not successful, aka. not 200)
request_status_code = int
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 = str
#: Specifies the type of thread. Can be used a `thread_type`. See :ref:`intro_threads` for more info
type = None
#: The thread's picture
photo = str
#: The name of the thread
name = str
#: Timestamp of last message
last_message_timestamp = str
#: Number of messages in the thread
message_count = int
def __init__(self, _type, uid, photo=None, name=None, last_message_timestamp=None, message_count=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
def __repr__(self):
return self.__unicode__()
def __unicode__(self):
return '<{} {} ({})>'.format(self.type.name, self.name, self.uid)
class User(Thread):
#: The profile url
url = str
#: The users first name
first_name = str
#: The users last name
last_name = str
#: Whether the user and the client are friends
is_friend = bool
#: The user's gender
gender = str
#: From 0 to 1. How close the client is to the user
affinity = float
#: The user's nickname
nickname = str
#: The clients nickname, as seen by the user
own_nickname = str
#: A :class:`ThreadColor`. The message color
color = None
#: The default emoji
emoji = str
def __init__(self, uid, url=None, first_name=None, last_name=None, is_friend=None, gender=None, affinity=None, nickname=None, own_nickname=None, color=None, emoji=None, **kwargs):
"""Represents a Facebook user. Inherits `Thread`"""
super(User, self).__init__(ThreadType.USER, uid, **kwargs)
self.url = url
self.first_name = first_name
self.last_name = last_name
self.is_friend = is_friend
self.gender = gender
self.affinity = affinity
self.nickname = nickname
self.own_nickname = own_nickname
self.color = color
self.emoji = emoji
class Group(Thread):
#: Unique list (set) of the group thread's participant user IDs
participants = set
#: Dict, containing user nicknames mapped to their IDs
nicknames = dict
#: A :class:`ThreadColor`. The groups's message color
color = None
#: The groups's default emoji
emoji = str
def __init__(self, uid, participants=None, nicknames=None, color=None, emoji=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
class Room(Group):
# Set containing user IDs of thread admins
admins = set
# True if users need approval to join
approval_mode = bool
# Set containing user IDs requesting to join
approval_requests = set
# Link for joining room
join_link = str
# True is room is not discoverable
privacy_mode = bool
def __init__(self, uid, admins=None, approval_mode=None, approval_requests=None, join_link=None, privacy_mode=None, **kwargs):
"""Represents a Facebook room. Inherits `Group`"""
super(Room, self).__init__(uid, **kwargs)
self.type = ThreadType.ROOM
if admins is None:
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
self.privacy_mode = privacy_mode
class Page(Thread):
#: The page's custom url
url = str
#: The name of the page's location city
city = str
#: Amount of likes the page has
likes = int
#: Some extra information about the page
sub_title = str
#: The page's category
category = str
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 = []
#: 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 dict with user's IDs as keys, and their :class:`MessageReaction` as values
reactions = {}
#: The actual message
text = None
#: A :class:`Sticker`
sticker = None
#: A list of attachments
attachments = []
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 = {}
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 = str
def __init__(self, uid=None):
"""Represents a Facebook attachment"""
self.uid = uid
class Sticker(Attachment):
#: The sticker-pack's ID
pack = None
#: Whether the sticker is animated
is_animated = False
# If the sticker is animated, the following should be present
#: URL to a medium spritemap
medium_sprite_image = None
#: URL to a large spritemap
large_sprite_image = None
#: The amount of frames present in the spritemap pr. row
frames_per_row = None
#: The amount of frames present in the spritemap pr. coloumn
frames_per_col = None
#: The frame rate the spritemap is intended to be played in
frame_rate = None
#: URL to the sticker's image
url = None
#: Width of the sticker
width = None
#: Height of the sticker
height = None
#: The sticker's label/name
label = None
def __init__(self, *args, **kwargs):
"""Represents a Facebook sticker that has been sent to a Facebook thread as an attachment"""
super(Sticker, self).__init__(*args, **kwargs)
class ShareAttachment(Attachment):
def __init__(self, **kwargs):
"""Represents a shared item (eg. URL) that has been sent as a Facebook attachment - *Currently Incomplete!*"""
super(ShareAttachment, self).__init__(**kwargs)
class FileAttachment(Attachment):
#: Url where you can download the file
url = str
#: Size of the file in bytes
size = int
#: Name of the file
name = str
#: Whether Facebook determines that this file may be harmful
is_malicious = bool
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(FileAttachment):
def __init__(self, **kwargs):
"""Represents an audio file that has been sent as a Facebook attachment - *Currently Incomplete!*"""
super(StickerAttachment, self).__init__(**kwargs)
class ImageAttachment(Attachment):
#: The extension of the original image (eg. 'png')
original_extension = str
#: Width of original image
width = int
#: Height of original image
height = int
#: Whether the image is animated
is_animated = bool
#: URL to a thumbnail of the image
thumbnail_url = str
#: URL to a medium preview of the image
preview_url = str
#: Width of the medium preview image
preview_width = int
#: Height of the medium preview image
preview_height = int
#: URL to a large preview of the image
large_preview_url = str
#: Width of the large preview image
large_preview_width = int
#: Height of the large preview image
large_preview_height = int
#: URL to an animated preview of the image (eg. for gifs)
animated_preview_url = str
#: Width of the animated preview image
animated_preview_width = int
#: Height of the animated preview image
animated_preview_height = int
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 = int
#: Width of original video
width = int
#: Height of original video
height = int
#: Length of video in milliseconds
duration = int
#: URL to very compressed preview video
preview_url = str
#: URL to a small preview image of the video
small_image_url = str
#: Width of the small preview image
small_image_width = int
#: Height of the small preview image
small_image_height = int
#: URL to a medium preview image of the video
medium_image_url = str
#: Width of the medium preview image
medium_image_width = int
#: Height of the medium preview image
medium_image_height = int
#: URL to a large preview image of the video
large_image_url = str
#: Width of the large preview image
large_image_width = int
#: Height of the large preview image
large_image_height = int
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 = str
#: The character where the mention starts
offset = int
#: The length of the mention
length = int
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 Enum(enum.Enum):
"""Used internally by fbchat to support enumerations"""
def __repr__(self):
# For documentation:
return '{}.{}'.format(type(self).__name__, self.name)
class ThreadType(Enum):
"""Used to specify what type of Facebook thread is being used. See :ref:`intro_threads` for more info"""
USER = 1
GROUP = 2
PAGE = 3
ROOM = 4
class ThreadLocation(Enum):
"""Used to specify where a thread is located (inbox, pending, archived, other)."""
INBOX = 'inbox'
PENDING = 'pending'
ARCHIVED = 'action: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 = ''
VIKING = '#44bec7'
GOLDEN_POPPY = '#ffc300'
RADICAL_RED = '#fa3c4c'
SHOCKING = '#d696bb'
PICTON_BLUE = '#6699cc'
FREE_SPEECH_GREEN = '#13cf13'
PUMPKIN = '#ff7e29'
LIGHT_CORAL = '#e68585'
MEDIUM_SLATE_BLUE = '#7646ff'
DEEP_SKY_BLUE = '#20cef5'
FERN = '#67b868'
CAMEO = '#d4a88c'
BRILLIANT_ROSE = '#ff5ca1'
BILOBA_FLOWER = '#a695c7'
class MessageReaction(Enum):
"""Used to specify a message reaction"""
LOVE = '😍'
SMILE = '😆'
WOW = '😮'
SAD = '😢'
ANGRY = '😠'
YES = '👍'
NO = '👎'
from ._core import Enum
from ._exception import FBchatException, FBchatFacebookError, FBchatUserError
from ._thread import ThreadType, ThreadLocation, ThreadColor, Thread
from ._user import TypingStatus, User, ActiveStatus
from ._group import Group, Room
from ._page import Page
from ._message import EmojiSize, MessageReaction, Mention, Message
from ._attachment import Attachment, UnsentMessage, ShareAttachment
from ._sticker import Sticker
from ._location import LocationAttachment, LiveLocationAttachment
from ._file import FileAttachment, AudioAttachment, ImageAttachment, VideoAttachment
from ._quick_reply import (
QuickReply,
QuickReplyText,
QuickReplyLocation,
QuickReplyPhoneNumber,
QuickReplyEmail,
)
from ._poll import Poll, PollOption
from ._plan import GuestStatus, Plan

View File

@@ -1,227 +0,0 @@
# -*- coding: UTF-8 -*-
from __future__ import unicode_literals
import re
import json
from time import time
from random import random
import warnings
import logging
from .models import *
# 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",
"Mozilla/5.0 (Windows NT 6.3; WOW64; ; NCT50_AAP285C84A1328) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.90 Safari/537.36",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/22.0.1207.1 Safari/537.1",
"Mozilla/5.0 (X11; CrOS i686 2268.111.0) AppleWebKit/536.11 (KHTML, like Gecko) Chrome/20.0.1132.57 Safari/536.11",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.6 (KHTML, like Gecko) Chrome/20.0.1092.0 Safari/536.6"
]
LIKES = {
'large': EmojiSize.LARGE,
'medium': EmojiSize.MEDIUM,
'small': EmojiSize.SMALL,
'l': EmojiSize.LARGE,
'm': EmojiSize.MEDIUM,
's': EmojiSize.SMALL
}
MessageReactionFix = {
'😍': ('0001f60d', '%F0%9F%98%8D'),
'😆': ('0001f606', '%F0%9F%98%86'),
'😮': ('0001f62e', '%F0%9F%98%AE'),
'😢': ('0001f622', '%F0%9F%98%A2'),
'😠': ('0001f620', '%F0%9F%98%A0'),
'👍': ('0001f44d', '%F0%9F%91%8D'),
'👎': ('0001f44e', '%F0%9F%91%8E')
}
GENDERS = {
# For standard requests
0: 'unknown',
1: 'female_singular',
2: 'male_singular',
3: 'female_singular_guess',
4: 'male_singular_guess',
5: 'mixed',
6: 'neuter_singular',
7: 'unknown_singular',
8: 'female_plural',
9: 'male_plural',
10: 'neuter_plural',
11: 'unknown_plural',
# 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/"
THREAD_SYNC = "https://www.facebook.com/ajax/mercury/thread_sync.php"
THREADS = "https://www.facebook.com/ajax/mercury/threadlist_info.php"
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"
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/"
EVENT_REMINDER = "https://www.facebook.com/ajax/eventreminder/create"
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: {}, {}'.format(repr(text), text.index('{')))
def get_decoded_r(r):
return get_decoded(r._content)
def get_decoded(content):
return content.decode(facebookEncoding)
def parse_json(content):
return json.loads(content)
def get_json(r):
return json.loads(strip_to_json(get_decoded_r(r)))
def digitToChar(digit):
if digit < 10:
return str(digit)
return chr(ord('a') + digit - 10)
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) + digitToChar(m)
return digitToChar(m)
def generateMessageID(client_id=None):
k = now()
l = int(random() * 4294967295)
return "<{}:{}-{}@mail.projektitan.com>".format(k, l, client_id)
def getSignatureID():
return hex(int(random() * 2147483648))
def generateOfflineThreadingID():
ret = now()
value = int(random() * 4294967295)
string = ("0000000000000000000000" + format(value, 'b'))[-22:]
msgs = format(ret, 'b') + string
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: {}'.format(repr(content)))
check_json(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

67
pyproject.toml Normal file
View File

@@ -0,0 +1,67 @@
[tool.black]
line-length = 88
[build-system]
requires = ["flit"]
build-backend = "flit.buildapi"
[tool.flit.metadata]
module = "fbchat"
author = "Taehoon Kim"
author-email = "carpedm20@gmail.com"
maintainer = "Mads Marquart"
maintainer-email = "madsmtm@gmail.com"
home-page = "https://github.com/carpedm20/fbchat/"
requires = [
"aenum~=2.0",
"attrs>=18.2",
"requests~=2.19",
"beautifulsoup4~=4.0",
]
description-file = "README.rst"
classifiers = [
"Development Status :: 3 - Alpha",
"Intended Audience :: Developers",
"Intended Audience :: Information Technology",
"License :: OSI Approved :: BSD License",
"Operating System :: OS Independent",
"Natural Language :: English",
"Programming Language :: Python",
"Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.4",
"Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy",
"Topic :: Communications :: Chat",
"Topic :: Internet :: WWW/HTTP",
"Topic :: Internet :: WWW/HTTP :: Dynamic Content",
"Topic :: Software Development :: Libraries",
"Topic :: Software Development :: Libraries :: Python Modules",
]
requires-python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, <4.0"
keywords = "Facebook FB Messenger Library Chat Api Bot"
license = "BSD 3-Clause"
[tool.flit.metadata.urls]
Documentation = "https://fbchat.readthedocs.io/"
Repository = "https://github.com/carpedm20/fbchat/"
[tool.flit.metadata.requires-extra]
test = [
"pytest~=4.0",
"six~=1.0",
]
docs = [
"sphinx~=2.0",
"sphinxcontrib-spelling~=4.0"
]
lint = [
"black",
]
tools = [
# Fork of bumpversion, see https://github.com/c4urself/bump2version
"bump2version~=0.5.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"

View File

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

View File

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

260
tests.py
View File

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

129
tests/conftest.py Normal file
View File

@@ -0,0 +1,129 @@
# -*- 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)

View File

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

BIN
tests/resources/audio.mp3 Normal file

Binary file not shown.

View File

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

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

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

BIN
tests/resources/image.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
tests/resources/image.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
tests/resources/video.mp4 Normal file

Binary file not shown.

55
tests/test_base.py Normal file
View File

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

101
tests/test_fetch.py Normal file
View File

@@ -0,0 +1,101 @@
# -*- 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
def test_fetch_threads(client1):
threads = client1.fetchThreads(limit=2)
assert len(threads) == 2
@pytest.mark.parametrize("emoji, emoji_size", EMOJI_LIST)
def test_fetch_message_emoji(client, emoji, emoji_size):
mid = client.sendEmoji(emoji, emoji_size)
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)

114
tests/test_plans.py Normal file
View File

@@ -0,0 +1,114 @@
# -*- 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)

107
tests/test_polls.py Normal file
View File

@@ -0,0 +1,107 @@
# -*- 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"

125
tests/test_send.py Normal file
View File

@@ -0,0 +1,125 @@
# -*- 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,164 @@
# -*- 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())