diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 00000000..3c6b03c3 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,67 @@ +version: 2 +workflows: + version: 2 + build: + jobs: + - test-python-3.5 + - test-python-3.6 + - test-python-3.7 + - test-python-3.8 +jobs: + test-python-3.5: &test-python-template + docker: + - image: circleci/python:3.5-browsers + working_directory: ~/facebook-sdk + steps: + - checkout + - run: + name: install virtualenv and dependencies + command: | + mkdir -p ~/venv + python -m venv ~/venv; + . ~/venv/bin/activate + pip install coverage pygments + pip install -e . + - run: + name: run linting + command: | + . ~/venv/bin/activate + if [ $(python -c "import platform; print(platform.python_version()[:3])") == "3.8" ]; then + pip install black doc8; + black -l 79 --check examples; + black -l 79 --check facebook; + black -l 79 --check test; + doc8 -q *.rst docs/*.rst; + fi; + - run: + name: download CodeClimate test reporter utility + command: | + curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter + chmod +x ./cc-test-reporter + - run: + name: run automated tests + command: | + . ~/venv/bin/activate + ./cc-test-reporter before-build + coverage run --source="facebook" -m unittest discover + coverage xml + ./cc-test-reporter format-coverage -t coverage.py -o "coverage/codeclimate.$CIRCLE_NODE_INDEX.json" + - store_artifacts: + path: test-reports + destination: test-reports + - deploy: + name: upload test coverage reports to CodeClimate + command: | + ./cc-test-reporter sum-coverage -o - -p $CIRCLE_NODE_TOTAL coverage/codeclimate.*.json | ./cc-test-reporter upload-coverage --debug -i - + test-python-3.6: + <<: *test-python-template + docker: + - image: circleci/python:3.6-browsers + test-python-3.7: + <<: *test-python-template + docker: + - image: circleci/python:3.7-browsers + test-python-3.8: + <<: *test-python-template + docker: + - image: circleci/python:3.8-browsers diff --git a/.github/issue_template.md b/.github/issue_template.md new file mode 100644 index 00000000..6e3af474 --- /dev/null +++ b/.github/issue_template.md @@ -0,0 +1,11 @@ +If you have a general question about how to use this SDK or Facebook's Graph API, you should be posting a question at https://groups.google.com/group/pythonforfacebook instead of creating an issue here. Bugs in the Graph API should be reported at https://developers.facebook.com/bugs/. + +PLEASE DELETE THE ABOVE AFTER READING IT AND FILL IN EACH OF THE FOLLOWING SECTIONS. + +### Version of the SDK being used + +### Expected Behavior + +### Actual Behavior + +### Steps to Reproduce diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index f1489733..75d11513 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -1,6 +1,5 @@ -======================================= -Contributing to the Facebook Python SDK -======================================= +Contributing +============ Use Github Pull Requests ------------------------ @@ -13,14 +12,13 @@ an unrelated area of the code). Code Style ---------- -Code *must* be compliant with `PEP8`_.Use the latest version of `pep8pypi`_ or -`flake8`_ to catch issues. +Code *must* be compliant with `PEP 8`_. Use the latest version of `black`_ +to catch formatting issues. Git commit messages should include `a summary and proper line wrapping`_. -.. _PEP8: http://www.python.org/dev/peps/pep-0008/ -.. _pep8pypi: https://pypi.python.org/pypi/pep8 -.. _flake8: https://pypi.python.org/pypi/flake8 +.. _PEP 8: https://www.python.org/dev/peps/pep-0008/ +.. _black: https://pypi.org/project/black/ .. _a summary and proper line wrapping: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html Update Tests and Documentation @@ -28,11 +26,3 @@ Update Tests and Documentation All non-trivial changes should include full test coverage. Please review the package's documentation to ensure that it is up to date with any changes. - -Questions? ----------- - -Visit the library's `Google Group`_. - -.. _Google Group: https://groups.google.com/group/pythonforfacebook - diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..d6456956 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.rst b/README.rst index b0dec6d8..f1456d90 100644 --- a/README.rst +++ b/README.rst @@ -11,38 +11,13 @@ Facebook authentication. You can read more about the Graph API by accessing its .. _Facebook JavaScript SDK: https://developers.facebook.com/docs/reference/javascript/ .. _official documentation: https://developers.facebook.com/docs/reference/api/ -Basic usage: +Licensing +========= -:: +This library uses the `Apache License, version 2.0`_. Please see the library's +individual files for more information. - graph = facebook.GraphAPI(oauth_access_token) - profile = graph.get_object("me") - friends = graph.get_connections("me", "friends") - graph.put_object("me", "feed", message="I am writing on my wall!") - -Photo uploads: - -:: - - graph = facebook.GraphAPI(oauth_access_token) - tags = json.dumps([{'x':50, 'y':50, 'tag_uid':12345}, {'x':10, 'y':60, 'tag_text':'a turtle'}]) - graph.put_photo(open('img.jpg'), 'Look at this cool photo!', album_id_or_None, tags=tags) - -If you are using the module within a web application with the JavaScript SDK, -you can also use the module to use Facebook for login, parsing the cookie set -by the JavaScript SDK for logged in users. For example, in Google AppEngine, -you could get the profile of the logged in user with: - -:: - - user = facebook.get_user_from_cookie(self.request.cookies, key, secret) - if user: - graph = facebook.GraphAPI(user["access_token"]) - profile = graph.get_object("me") - friends = graph.get_connections("me", "friends") - - -You can see a full AppEngine example application in examples/appengine. +.. _Apache License, version 2.0: https://www.apache.org/licenses/LICENSE-2.0 Reporting Issues ================ @@ -51,14 +26,14 @@ If you have bugs or other issues specifically pertaining to this library, file them `here`_. Bugs with the Graph API should be filed on `Facebook's bugtracker`_. -.. _here: https://github.com/pythonforfacebook/facebook-sdk/issues +.. _here: https://github.com/mobolic/facebook-sdk/issues .. _Facebook's bugtracker: https://developers.facebook.com/bugs/ Support & Discussion ==================== -Documentation is available at https://facebook-sdk.readthedocs.org/en/latest/. +Documentation is available at https://facebook-sdk.readthedocs.io/en/latest/. Have a question? Need help? Visit the library's `Google Group`_. diff --git a/docs/api.rst b/docs/api.rst new file mode 100644 index 00000000..e83fe5fb --- /dev/null +++ b/docs/api.rst @@ -0,0 +1,344 @@ +============= +API Reference +============= + +This page contains specific information on the SDK's classes, methods and +functions. + +class facebook.GraphAPI +======================= + +A client for the Facebook Graph API. The Graph API is made up of the objects or +nodes in Facebook (e.g., people, pages, events, photos) and the connections or +edges between them (e.g., friends, photo tags, and event RSVPs). This client +provides access to those primitive types in a generic way. + +You can read more about `Facebook's Graph API here`_. + +.. _Facebook's Graph API here: https://developers.facebook.com/docs/graph-api + +**Parameters** + +* ``access_token`` – A ``string`` that identifies a user, app, or page and can + be used by the app to make graph API calls. + `Read more about access tokens here`_. +* ``timeout`` - A ``float`` describing (in seconds) how long the client will be + waiting for a response from Facebook's servers. `See more here`_. +* ``version`` - A ``string`` describing the `version of Facebook's Graph API to + use`_. The default version is the oldest current version. It is used if + the version keyword argument is not provided. +* ``proxies`` - A ``dict`` with proxy-settings that Requests should use. + `See Requests documentation`_. +* ``session`` - A `Requests Session object`_. +* ``app_secret`` - A ``string`` containing the secret key of your + app. If both ``access_token`` and ``app_secret`` are present this will be + used to compute an `application secret proof`_ that will be sent on every + API request. + + +.. _Read more about access tokens here: https://developers.facebook.com/docs/facebook-login/access-tokens +.. _See more here: http://docs.python-requests.org/en/latest/user/quickstart/#timeouts +.. _version of Facebook's Graph API to use: https://developers.facebook.com/docs/apps/changelog#versions +.. _See Requests documentation: http://www.python-requests.org/en/latest/user/advanced/#proxies +.. _Requests Session object: http://docs.python-requests.org/en/master/user/advanced/#session-objects +.. _application secret proof: https://developers.facebook.com/docs/graph-api/securing-requests + +**Example** + +.. code-block:: python + + import facebook + + graph = facebook.GraphAPI(access_token="your_token", version="2.12") + +Methods +------- + +get_object +^^^^^^^^^^ + +Returns the given object from the graph as a ``dict``. A list of +`supported objects can be found here`_. + +.. _supported objects can be found here: https://developers.facebook.com/docs/graph-api/reference/ + +**Parameters** + +* ``id`` – A ``string`` that is a unique ID for that particular resource. +* ``**args`` (optional) - keyword args to be passed as query params + +**Examples** + +.. code-block:: python + + # Get the message from a post. + post = graph.get_object(id='post_id', fields='message') + print(post['message']) + +.. code-block:: python + + # Retrieve the number of people who say that they are attending or + # declining to attend a specific event. + event = graph.get_object(id='event_id', + fields='attending_count,declined_count') + print(event['attending_count']) + print(event['declined_count']) + +.. code-block:: python + + # Retrieve information about a website or page: + # https://developers.facebook.com/docs/graph-api/reference/url/ + # Note that URLs need to be properly encoded with the "quote" function + # of urllib (Python 2) or urllib.parse (Python 3). + site_info = graph.get_object(id="https%3A//mobolic.com", + fields="og_object") + print(site_info["og_object"]["description"]) + +get_objects +^^^^^^^^^^^ + +Returns all of the given objects from the graph as a ``dict``. Each given ID +maps to an object. + +**Parameters** + +* ``ids`` – A ``list`` containing IDs for multiple resources. +* ``**args`` (optional) - keyword args to be passed as query params + +**Examples** + +.. code-block:: python + + # Get the time two different posts were created. + post_ids = ['post_id_1', 'post_id_2'] + posts = graph.get_objects(ids=post_ids, fields="created_time") + + for post in posts: + print(post['created_time']) + +.. code-block:: python + + # Get the number of people attending or who have declined to attend + # two different events. + event_ids = ['event_id_1', 'event_id_2'] + events = graph.get_objects(ids=event_ids, fields='attending_count,declined_count') + + for event in events: + print(event['declined_count']) + +search +^^^^^^ + +https://developers.facebook.com/docs/places/search + +Valid types are: place, placetopic + +**Parameters** + +* ``type`` – A ``string`` containing a valid type. +* ``**args`` (optional) - keyword args to be passed as query params + +**Example** + +.. code-block:: python + + # Search for places near 1 Hacker Way in Menlo Park, California. + places = graph.search(type='place', + center='37.4845306,-122.1498183', + fields='name,location') + + # Each given id maps to an object the contains the requested fields. + for place in places['data']: + print('%s %s' % (place['name'].encode(),place['location'].get('zip'))) + +get_connections +^^^^^^^^^^^^^^^ + +Returns all connections for a given object as a ``dict``. + +**Parameters** + +* ``id`` – A ``string`` that is a unique ID for that particular resource. +* ``connection_name`` - A ``string`` that specifies the connection or edge + between objects, e.g., feed, friends, groups, likes, posts. If left empty, + ``get_connections`` will simply return the authenticated user's basic + information. + +**Examples** + +.. code-block:: python + + # Get the active user's friends. + friends = graph.get_connections(id='me', connection_name='friends') + + # Get the comments from a post. + comments = graph.get_connections(id='post_id', connection_name='comments') + + +get_all_connections +^^^^^^^^^^^^^^^^^^^ + +Iterates over all pages returned by a get_connections call and yields the +individual items. + +**Parameters** + +* ``id`` – A ``string`` that is a unique ID for that particular resource. +* ``connection_name`` - A ``string`` that specifies the connection or edge + between objects, e.g., feed, friends, groups, likes, posts. + +put_object +^^^^^^^^^^ + +Writes the given object to the graph, connected to the given parent. + +**Parameters** + +* ``parent_object`` – A ``string`` that is a unique ID for that particular + resource. The ``parent_object`` is the parent of a connection or edge. E.g., + profile is the parent of a feed, and a post is the parent of a comment. +* ``connection_name`` - A ``string`` that specifies the connection or edge + between objects, e.g., feed, friends, groups, likes, posts. + +**Examples** + +.. code-block:: python + + # Write 'Hello, world' to the active user's wall. + graph.put_object(parent_object='me', connection_name='feed', + message='Hello, world') + + # Add a link and write a message about it. + graph.put_object( + parent_object="me", + connection_name="feed", + message="This is a great website. Everyone should visit it.", + link="https://www.facebook.com") + + # Write a comment on a post. + graph.put_object(parent_object='post_id', connection_name='comments', + message='First!') + +put_comment +^^^^^^^^^^^ + +Writes the given message as a comment on an object. + +**Parameters** + +* ``object_id`` - A ``string`` that is a unique id for a particular resource. +* ``message`` - A ``string`` that will be posted as the comment. + +**Example** + +.. code-block:: python + + graph.put_comment(object_id='post_id', message='Great post...') + + +put_like +^^^^^^^^ + +Writes a like to the given object. + +**Parameters** + +* ``object_id`` - A ``string`` that is a unique id for a particular resource. + +**Example** + +.. code-block:: python + + graph.put_like(object_id='comment_id') + + +put_photo +^^^^^^^^^ + +https://developers.facebook.com/docs/graph-api/reference/user/photos#publish + +Upload an image using multipart/form-data. Returns JSON with the IDs of the +photo and its post. + +**Parameters** + + * ``image`` - A file object representing the image to be uploaded. + * ``album_path`` - A path representing where the image should be uploaded. + Defaults to `/me/photos` which creates/uses a custom album for each + Facebook application. + +**Examples** + +.. code-block:: python + + # Upload an image with a caption. + graph.put_photo(image=open('img.jpg', 'rb'), + message='Look at this cool photo!') + + # Upload a photo to an album. + graph.put_photo(image=open("img.jpg", 'rb'), + album_path=album_id + "/photos") + + # Upload a profile photo for a Page. + graph.put_photo(image=open("img.jpg", 'rb'), + album_path=page_id + "/picture") + +delete_object +^^^^^^^^^^^^^ + +Deletes the object with the given ID from the graph. + +**Parameters** + +* ``id`` - A ``string`` that is a unique ID for a particular resource. + +**Example** + +.. code-block:: python + + graph.delete_object(id='post_id') + +get_permissions +^^^^^^^^^^^^^^^ + +https://developers.facebook.com/docs/graph-api/reference/user/permissions/ + +Returns the permissions granted to the app by the user with the given ID as a +``set``. + +**Parameters** + +* ``user_id`` - A ``string`` containing a user's unique ID. + +**Example** + +.. code-block:: python + + # Figure out whether the specified user has granted us the + # "public_profile" permission. + permissions = graph.get_permissions(user_id=12345) + print('public_profile' in permissions) + +get_auth_url +^^^^^^^^^^^^ + +https://developers.facebook.com/docs/facebook-login/manually-build-a-login-flow + +Returns a Facebook login URL used to request an access token and permissions. + +**Parameters** + +* ``app_id`` - A ``string`` containing a Facebook application ID. +* ``canvas_url`` - A ``string`` containing the URL where Facebook should + redirect after successful authentication. +* ``perms`` - An optional ``list`` of requested Facebook permissions. + +**Example** + +.. code-block:: python + + app_id = "1231241241" + canvas_url = "https://domain.com/that-handles-auth-response/" + perms = ["manage_pages","publish_pages"] + fb_login_url = graph.get_auth_url(app_id, canvas_url, perms) + print(fb_login_url) diff --git a/docs/changes.rst b/docs/changes.rst new file mode 100644 index 00000000..5dec2cbc --- /dev/null +++ b/docs/changes.rst @@ -0,0 +1,77 @@ +========= +Changelog +========= + +Version 4.0.0 (unreleased) +========================== +- Add support for Python 3.8. +- Remove support for Python 2.7 and 3.4. +- Add support for Graph API versions 3.2, 3.3, 4.0, 5.0, 6.0, 7.0 and 8.0. +- Remove support for Graph API versions 2.8, 2.9, 2.10, 2.11, 2.12, and 3.0. +- Change default Graph API version to 2.10. +- Add support for securing Graph API Calls with a proof based on the + application secret (#454). +- Add subcodes to GraphAPIError objects. + +Version 3.1.0 (2018-11-06) +========================== +- Add support for Graph API version 3.1. +- Remove support for Graph API version 2.7. +- Change default Graph API version to 2.8. + +Version 3.0.0 (2018-08-08) +========================== + - Add support for Python 3.6 and 3.7. + - Remove support for Python 2.6 and 3.3. + - Add support for Graph API versions 2.8, 2.9, 2.10, 2.11, 2.12, and 3.0. + - Remove support for Graph API versions 2.1, 2.2, 2.3, 2.4, 2.5, and 2.6. + - Change default Graph API version to 2.7. + - Add support for requests' sessions (#201). + - Add versioning to access token endpoints (#322). + - Add new `get_all_connections` method to make pagination easier (#337). + - Add new `get_permissions` method to retrieve permissions that a user has + granted an application (#264, #342). + - Remove `put_wall_post` method. Use `put_object` instead. + - Add search method (#362). + - Rename `auth_url` method to `get_auth_url` and move it into the Graph API + object (#377, #378, #422). + +Version 2.0.0 (2016-08-08) +========================== + - Add support for Graph API versions 2.6 and 2.7. + - Remove support for Graph API version 2.0 and FQL. + - Change default Graph API version to 2.1. + - Fix bug with debug_access_token method not working when the + GraphAPI object's access token was set (#276). + - Allow offline generation of application access tokens. + +Version 1.0.0 (2016-04-01) +========================== + + - Python 3 support. + - More comprehensive test coverage. + - Full Unicode support. + - Better exception handling. + - Vastly improved documentation. + +Version 0.4.0 (2012-10-15) +========================== + + - Add support for deleting application requests. + - Fix minor documentation error in README. + - Verify signed request parsing succeeded when creating OAuth token. + - Convert README to ReStructuredText. + +Version 0.3.2 (2012-07-28) +========================== + + - Add support for state parameters in auth dialog URLs. + - Fixes bug with Unicode app secrets. + - Add optional timeout support for faster API requests. + - Random PEP8 compliance fixes. + +Version 0.3.1 (2012-05-16) +========================== + + - Minor documentation updates. + - Removes the develop branch in favor of named feature branches. diff --git a/docs/conf.py b/docs/conf.py index 3f396bb1..70b10a24 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -3,7 +3,8 @@ # Facebook SDK for Python documentation build configuration file, created by # sphinx-quickstart on Mon Oct 15 01:01:28 2012. # -# This file is execfile()d with the current directory set to its containing dir. +# 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. @@ -11,20 +12,20 @@ # All configuration values have a default; values that are commented out # serve to show the default. -import sys, os +exec(open("../facebook/version.py").read()) # 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. -#sys.path.insert(0, os.path.abspath('.')) +# sys.path.insert(0, os.path.abspath('.')) -# -- General configuration ----------------------------------------------------- +# -- General configuration ---------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' +# needs_sphinx = '1.0' -# Add any Sphinx extension module names here, as strings. They can be extensions -# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [] # Add any paths that contain templates here, relative to this directory. @@ -34,60 +35,61 @@ source_suffix = '.rst' # The encoding of source files. -#source_encoding = 'utf-8-sig' +# source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = u'Facebook SDK for Python' -copyright = u'2010-2013, Facebook, Python for Facebook developers' +copyright = u'2010 Facebook, 2015 Mobolic' # 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 = '0.4' +version = '.'.join(__version__.split('.')[:2]) # noqa: F821 # The full version, including alpha/beta/rc tags. -release = '0.4.0' +release = __version__ # noqa: F821 # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. -#language = None +# language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: -#today = '' +# today = '' # Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' +# today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ['_build'] -# The reST default role (used for this markup: `text`) to use for all documents. -#default_role = None +# The reST default role (used for this markup: `text`) to use for all +# documents. +# default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True +# add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). -#add_module_names = True +# add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. -#show_authors = False +# show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] +# modindex_common_prefix = [] -# -- 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. @@ -96,26 +98,26 @@ # 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 = {} # Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] +# html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". -#html_title = None +# html_title = None # A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None +# html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. -#html_logo = None +# html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. -#html_favicon = None +# html_favicon = None # 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, @@ -124,119 +126,121 @@ # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' +# html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. -#html_use_smartypants = True +# html_use_smartypants = True # Custom sidebar templates, maps document names to template names. -#html_sidebars = {} +# html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. -#html_additional_pages = {} +# html_additional_pages = {} # If false, no module index is generated. -#html_domain_indices = True +# html_domain_indices = True # If false, no index is generated. -#html_use_index = True +# html_use_index = True # If true, the index is split into individual pages for each letter. -#html_split_index = False +# html_split_index = False # If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True +# html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True +# html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True +# html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. -#html_use_opensearch = '' +# html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None +# html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'FacebookSDKforPythondoc' -# -- Options for LaTeX output -------------------------------------------------- +# -- Options for LaTeX output ------------------------------------------------- latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', + # The paper size ('letterpaper' or 'a4paper'). + # 'papersize': 'letterpaper', -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', + # The font size ('10pt', '11pt' or '12pt'). + # 'pointsize': '10pt', -# Additional stuff for the LaTeX preamble. -#'preamble': '', + # Additional stuff for the LaTeX preamble. + # 'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, author, documentclass [howto/manual]). +# (source start file, target name, title, author, documentclass +# [howto/manual]). latex_documents = [ - ('index', 'FacebookSDKforPython.tex', u'Facebook SDK for Python Documentation', - u'Facebook, Python for Facebook developers', 'manual'), + ('index', 'FacebookSDKforPython.tex', + u'Facebook SDK for Python Documentation', + u'Martey Dodoo', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. -#latex_logo = None +# latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. -#latex_use_parts = False +# latex_use_parts = False # If true, show page references after internal links. -#latex_show_pagerefs = False +# latex_show_pagerefs = False # If true, show URL addresses after external links. -#latex_show_urls = False +# latex_show_urls = False # Documents to append as an appendix to all manuals. -#latex_appendices = [] +# latex_appendices = [] # If false, no module index is generated. -#latex_domain_indices = True +# latex_domain_indices = True -# -- 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 = [ ('index', 'facebooksdkforpython', u'Facebook SDK for Python Documentation', - [u'Facebook, Python for Facebook developers'], 1) + [u'Martey Dodoo'], 1) ] # If true, show URL addresses after external links. -#man_show_urls = False +# man_show_urls = False -# -- 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 = [ ('index', 'FacebookSDKforPython', u'Facebook SDK for Python Documentation', - u'Facebook, Python for Facebook developers', 'FacebookSDKforPython', 'One line description of project.', + u'Martey Dodoo', 'FacebookSDKforPython', 'One line description of project.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. -#texinfo_appendices = [] +# texinfo_appendices = [] # If false, no module index is generated. -#texinfo_domain_indices = True +# texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' +# texinfo_show_urls = 'footnote' diff --git a/docs/index.rst b/docs/index.rst index 4105b041..61da73f5 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,24 +1,21 @@ -.. Facebook SDK for Python documentation master file, created by - sphinx-quickstart on Mon Oct 15 01:01:28 2012. - You can adapt this file completely to your liking, but it should at least - contain the root `toctree` directive. - -Welcome to Facebook SDK for Python's documentation! -=================================================== - -Contents: +======================= +Facebook SDK for Python +======================= .. toctree:: :maxdepth: 2 - intro install - - -Indices and tables -================== - -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` - + integration + support + api + changes + +This client library is designed to support the `Facebook Graph API`_ and the +official `Facebook JavaScript SDK`_, which is the canonical way to implement +Facebook authentication. You can read more about the Graph API by accessing its +`official documentation`_. + +.. _Facebook Graph API: https://developers.facebook.com/docs/reference/api/ +.. _Facebook JavaScript SDK: https://developers.facebook.com/docs/reference/javascript/ +.. _official documentation: https://developers.facebook.com/docs/reference/api/ diff --git a/docs/install.rst b/docs/install.rst index f017eef3..4f612ef7 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -2,8 +2,34 @@ Installation ============ -We recommend using `pip`_ to install the SDK: +The SDK currently supports Python 3.5+. The `requests`_ package is required. +We recommend using `pip`_ and `virtualenv`_ to install the SDK. Please note +that the SDK's Python package is called **facebook-sdk**. + +Installing from Git +=================== + +For the newest features, you should install the SDK directly from Git. + +.. code-block:: shell + + virtualenv facebookenv + source facebookenv/bin/activate + pip install -e git+https://github.com/mobolic/facebook-sdk.git#egg=facebook-sdk + +Installing a Released Version +============================= + +If your application requires maximum stability, you will want to use a version +of the SDK that has been officially released. + +.. code-block:: shell + + virtualenv facebookenv + source facebookenv/bin/activate pip install facebook-sdk -.. _pip: http://www.pip-installer.org/ +.. _requests: https://pypi.python.org/pypi/requests +.. _pip: https://pip.pypa.io/ +.. _virtualenv: https://virtualenv.pypa.io/ diff --git a/docs/integration.rst b/docs/integration.rst new file mode 100644 index 00000000..14038774 --- /dev/null +++ b/docs/integration.rst @@ -0,0 +1,18 @@ +========================================= +Integrating the SDK with Other Frameworks +========================================= + +Flask +===== +The examples directory contains an example of using the SDK in Flask. + + +Google App Engine +================= + +Because the SDK uses `requests` (which requires socket support in Google App +Engine), you will need to enable billing. + +Tornado +======= +The examples directory contains an example of using the SDK in Tornado. diff --git a/docs/intro.rst b/docs/intro.rst deleted file mode 100644 index f54ca4b4..00000000 --- a/docs/intro.rst +++ /dev/null @@ -1,12 +0,0 @@ -============ -Introduction -============ - -This client library is designed to support the `Facebook Graph API`_ and the -official `Facebook JavaScript SDK`_, which is the canonical way to implement -Facebook authentication. You can read more about the Graph API by accessing its -`official documentation`_. - -.. _Facebook Graph API: https://developers.facebook.com/docs/reference/api/ -.. _Facebook JavaScript SDK: https://developers.facebook.com/docs/reference/javascript/ -.. _official documentation: https://developers.facebook.com/docs/reference/api/ diff --git a/docs/support.rst b/docs/support.rst new file mode 100644 index 00000000..449affe9 --- /dev/null +++ b/docs/support.rst @@ -0,0 +1,26 @@ +===================== +Support & Development +===================== + +Mailing List +============ + +Questions about the SDK should be sent to its `Google Group`_. + +.. _Google Group: https://groups.google.com/group/pythonforfacebook + +Reporting Bugs +============== + +Bugs with the SDK should be reported on the `issue tracker at Github`_. Bugs +with Facebook's Graph API should be reported on `Facebook's bugtracker`_. + +.. _issue tracker at Github: https://github.com/mobolic/facebook-sdk/issues +.. _Facebook's bugtracker: https://developers.facebook.com/x/bugs/ + +Security Issues +--------------- + +Security issues with the SDK that would adversely affect users if reported +publicly should be sent through private email to the project maintainer at +martey @ marteydodoo.com (GPG key ID is 0x2cd700988f74c455). diff --git a/examples/appengine/app.yaml b/examples/appengine/app.yaml deleted file mode 100644 index 7b1b54de..00000000 --- a/examples/appengine/app.yaml +++ /dev/null @@ -1,13 +0,0 @@ -application: facebook-example-py27 -version: 1 -runtime: python27 -api_version: 1 -threadsafe: true - -handlers: -- url: /.* - script: example.app - -libraries: -- name: jinja2 - version: latest diff --git a/examples/appengine/example.html b/examples/appengine/example.html deleted file mode 100644 index 982d5ec2..00000000 --- a/examples/appengine/example.html +++ /dev/null @@ -1,43 +0,0 @@ - - - - - Facebook Example - - - - - {% if current_user %} -

-

Hello, {{ current_user.name|escape }}

- {% endif %} - -
- - {% if current_user %} -
- Upload photo test: -
- Enter URL to URLFetch from: - -
-
- {% endif %} - - - diff --git a/examples/appengine/example.py b/examples/appengine/example.py deleted file mode 100644 index 2db52157..00000000 --- a/examples/appengine/example.py +++ /dev/null @@ -1,160 +0,0 @@ -#!/usr/bin/env python -# -# Copyright 2010 Facebook -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -A barebones AppEngine application that uses Facebook for login. - -1. Make sure you add a copy of facebook.py (from python-sdk/src/) - into this directory so it can be imported. -2. Don't forget to tick Login With Facebook on your facebook app's - dashboard and place the app's url wherever it is hosted -3. Place a random, unguessable string as a session secret below in - config dict. -4. Fill app id and app secret. -5. Change the application name in app.yaml. - -""" -FACEBOOK_APP_ID = "your app id" -FACEBOOK_APP_SECRET = "your app secret" - -import facebook -import webapp2 -import os -import jinja2 -import urllib2 - -from google.appengine.ext import db -from webapp2_extras import sessions - -config = {} -config['webapp2_extras.sessions'] = dict(secret_key='') - - -class User(db.Model): - id = db.StringProperty(required=True) - created = db.DateTimeProperty(auto_now_add=True) - updated = db.DateTimeProperty(auto_now=True) - name = db.StringProperty(required=True) - profile_url = db.StringProperty(required=True) - access_token = db.StringProperty(required=True) - - -class BaseHandler(webapp2.RequestHandler): - """Provides access to the active Facebook user in self.current_user - - The property is lazy-loaded on first access, using the cookie saved - by the Facebook JavaScript SDK to determine the user ID of the active - user. See http://developers.facebook.com/docs/authentication/ for - more information. - """ - @property - def current_user(self): - if self.session.get("user"): - # User is logged in - return self.session.get("user") - else: - # Either used just logged in or just saw the first page - # We'll see here - cookie = facebook.get_user_from_cookie(self.request.cookies, - FACEBOOK_APP_ID, - FACEBOOK_APP_SECRET) - if cookie: - # Okay so user logged in. - # Now, check to see if existing user - user = User.get_by_key_name(cookie["uid"]) - if not user: - # Not an existing user so get user info - graph = facebook.GraphAPI(cookie["access_token"]) - profile = graph.get_object("me") - user = User( - key_name=str(profile["id"]), - id=str(profile["id"]), - name=profile["name"], - profile_url=profile["link"], - access_token=cookie["access_token"] - ) - user.put() - elif user.access_token != cookie["access_token"]: - user.access_token = cookie["access_token"] - user.put() - # User is now logged in - self.session["user"] = dict( - name=user.name, - profile_url=user.profile_url, - id=user.id, - access_token=user.access_token - ) - return self.session.get("user") - return None - - def dispatch(self): - """ - This snippet of code is taken from the webapp2 framework documentation. - See more at - http://webapp-improved.appspot.com/api/webapp2_extras/sessions.html - - """ - self.session_store = sessions.get_store(request=self.request) - try: - webapp2.RequestHandler.dispatch(self) - finally: - self.session_store.save_sessions(self.response) - - @webapp2.cached_property - def session(self): - """ - This snippet of code is taken from the webapp2 framework documentation. - See more at - http://webapp-improved.appspot.com/api/webapp2_extras/sessions.html - - """ - return self.session_store.get_session() - - -class HomeHandler(BaseHandler): - def get(self): - template = jinja_environment.get_template('example.html') - self.response.out.write(template.render(dict( - facebook_app_id=FACEBOOK_APP_ID, - current_user=self.current_user - ))) - - def post(self): - url = self.request.get('url') - file = urllib2.urlopen(url) - graph = facebook.GraphAPI(self.current_user['access_token']) - response = graph.put_photo(file, "Test Image") - photo_url = ("http://www.facebook.com/" - "photo.php?fbid={0}".format(response['id'])) - self.redirect(str(photo_url)) - - -class LogoutHandler(BaseHandler): - def get(self): - if self.current_user is not None: - self.session['user'] = None - - self.redirect('/') - -jinja_environment = jinja2.Environment( - loader=jinja2.FileSystemLoader(os.path.dirname(__file__)) -) - -app = webapp2.WSGIApplication( - [('/', HomeHandler), ('/logout', LogoutHandler)], - debug=True, - config=config -) diff --git a/examples/flask/app/__init__.py b/examples/flask/app/__init__.py new file mode 100644 index 00000000..0329ab2d --- /dev/null +++ b/examples/flask/app/__init__.py @@ -0,0 +1,8 @@ +from flask import Flask +from flask.ext.sqlalchemy import SQLAlchemy + +app = Flask(__name__) +app.config.from_object("config") +db = SQLAlchemy(app) + +from app import views, models # noqa: E402,F401 diff --git a/examples/flask/app/models.py b/examples/flask/app/models.py new file mode 100644 index 00000000..0213f2c3 --- /dev/null +++ b/examples/flask/app/models.py @@ -0,0 +1,19 @@ +from datetime import datetime + +from app import db + + +class User(db.Model): + __tablename__ = "users" + + id = db.Column(db.String, nullable=False, primary_key=True) + created = db.Column(db.DateTime, default=datetime.utcnow, nullable=False) + updated = db.Column( + db.DateTime, + default=datetime.utcnow, + nullable=False, + onupdate=datetime.utcnow, + ) + name = db.Column(db.String, nullable=False) + profile_url = db.Column(db.String, nullable=False) + access_token = db.Column(db.String, nullable=False) diff --git a/examples/flask/app/static/css/style.css b/examples/flask/app/static/css/style.css new file mode 100644 index 00000000..035afb25 --- /dev/null +++ b/examples/flask/app/static/css/style.css @@ -0,0 +1,22 @@ +body { + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + text-align: center; +} + +.center { + margin: auto; + position: absolute; + top: 0; left: 0; bottom: 0; right: 0; + width: 50%; + height: 50%; + min-width: 200px; + max-width: 400px; + padding: 40px; +} + +.circle-image { + width: 200px; + height: 200px; + border-radius: 100%; + display: block; +} diff --git a/examples/flask/app/templates/base.html b/examples/flask/app/templates/base.html new file mode 100644 index 00000000..544a6e4a --- /dev/null +++ b/examples/flask/app/templates/base.html @@ -0,0 +1,57 @@ + + + + + + + + + + + + + {{ app_name }} + + + + + + + + + +
+ {% block content %} {% endblock %} +
+ + + diff --git a/examples/flask/app/templates/index.html b/examples/flask/app/templates/index.html new file mode 100644 index 00000000..bdf46ba3 --- /dev/null +++ b/examples/flask/app/templates/index.html @@ -0,0 +1,17 @@ +{% extends "base.html" %} +{% block content %} +
+

+

+

Hello, {{ user['name'] }}.

+ Log out + + +
+{% endblock %} diff --git a/examples/flask/app/templates/login.html b/examples/flask/app/templates/login.html new file mode 100644 index 00000000..a4269dfc --- /dev/null +++ b/examples/flask/app/templates/login.html @@ -0,0 +1,10 @@ +{% extends "base.html" %} +{% block content %} +
+

Python Facebook SDK

+

Flask example

+ +
+{% endblock %} diff --git a/examples/flask/app/views.py b/examples/flask/app/views.py new file mode 100644 index 00000000..d18e311d --- /dev/null +++ b/examples/flask/app/views.py @@ -0,0 +1,95 @@ +from facebook import get_user_from_cookie, GraphAPI +from flask import g, render_template, redirect, request, session, url_for + +from app import app, db +from .models import User + +# Facebook app details +FB_APP_ID = "" +FB_APP_NAME = "" +FB_APP_SECRET = "" + + +@app.route("/") +def index(): + # If a user was set in the get_current_user function before the request, + # the user is logged in. + if g.user: + return render_template( + "index.html", app_id=FB_APP_ID, app_name=FB_APP_NAME, user=g.user + ) + # Otherwise, a user is not logged in. + return render_template("login.html", app_id=FB_APP_ID, name=FB_APP_NAME) + + +@app.route("/logout") +def logout(): + """Log out the user from the application. + + Log out the user from the application by removing them from the + session. Note: this does not log the user out of Facebook - this is done + by the JavaScript SDK. + """ + session.pop("user", None) + return redirect(url_for("index")) + + +@app.before_request +def get_current_user(): + """Set g.user to the currently logged in user. + + Called before each request, get_current_user sets the global g.user + variable to the currently logged in user. A currently logged in user is + determined by seeing if it exists in Flask's session dictionary. + + If it is the first time the user is logging into this application it will + create the user and insert it into the database. If the user is not logged + in, None will be set to g.user. + """ + + # Set the user in the session dictionary as a global g.user and bail out + # of this function early. + if session.get("user"): + g.user = session.get("user") + return + + # Attempt to get the short term access token for the current user. + result = get_user_from_cookie( + cookies=request.cookies, app_id=FB_APP_ID, app_secret=FB_APP_SECRET + ) + + # If there is no result, we assume the user is not logged in. + if result: + # Check to see if this user is already in our database. + user = User.query.filter(User.id == result["uid"]).first() + + if not user: + # Not an existing user so get info + graph = GraphAPI(result["access_token"]) + profile = graph.get_object("me") + if "link" not in profile: + profile["link"] = "" + + # Create the user and insert it into the database + user = User( + id=str(profile["id"]), + name=profile["name"], + profile_url=profile["link"], + access_token=result["access_token"], + ) + db.session.add(user) + elif user.access_token != result["access_token"]: + # If an existing user, update the access token + user.access_token = result["access_token"] + + # Add the user to the current session + session["user"] = dict( + name=user.name, + profile_url=user.profile_url, + id=user.id, + access_token=user.access_token, + ) + + # Commit changes to the database and set the user as a global g.user + db.session.commit() + g.user = session.get("user", None) diff --git a/examples/flask/config.py b/examples/flask/config.py new file mode 100644 index 00000000..1f383d55 --- /dev/null +++ b/examples/flask/config.py @@ -0,0 +1,11 @@ +from os import path + +# App details +BASE_DIRECTORY = path.abspath(path.dirname(__file__)) +DEBUG = True +SECRET_KEY = "keep_it_like_a_secret" + +# Database details +SQLALCHEMY_DATABASE_URI = "{0}{1}".format( + "sqlite:///", path.join(BASE_DIRECTORY, "app.db") +) diff --git a/examples/flask/requirements.txt b/examples/flask/requirements.txt new file mode 100644 index 00000000..e21b69c0 --- /dev/null +++ b/examples/flask/requirements.txt @@ -0,0 +1,3 @@ +facebook-sdk==3.1.0 +flask>=0.12.3 +flask-sqlalchemy==2.1 diff --git a/examples/flask/run.py b/examples/flask/run.py new file mode 100644 index 00000000..16fb876e --- /dev/null +++ b/examples/flask/run.py @@ -0,0 +1,5 @@ +#!/usr/bin/env python +from app import app, db + +db.create_all() +app.run(host="0.0.0.0", port=8000) diff --git a/examples/get_posts.py b/examples/get_posts.py new file mode 100644 index 00000000..850e287d --- /dev/null +++ b/examples/get_posts.py @@ -0,0 +1,40 @@ +""" +A simple example script to get all posts on a user's timeline. +Originally created by Mitchell Stewart. + +""" +import facebook +import requests + + +def some_action(post): + """Here you might want to do something with each post. E.g. grab the + post's message (post['message']) or the post's picture (post['picture']). + In this implementation we just print the post's created time. + """ + print(post["created_time"]) + + +# You'll need an access token here to do anything. You can get a temporary one +# here: https://developers.facebook.com/tools/explorer/ +access_token = "" +# Look at Bill Gates's profile for this example by using his Facebook id. +user = "BillGates" + +graph = facebook.GraphAPI(access_token) +profile = graph.get_object(user) +posts = graph.get_connections(profile["id"], "posts") + +# Wrap this block in a while loop so we can keep paginating requests until +# finished. +while True: + try: + # Perform some action on each post in the collection we receive from + # Facebook. + [some_action(post=post) for post in posts["data"]] + # Attempt to make a request to the next page of data, if it exists. + posts = requests.get(posts["paging"]["next"]).json() + except KeyError: + # When there are no more pages (['paging']['next']), break from the + # loop and end the script. + break diff --git a/examples/newsfeed/app.yaml b/examples/newsfeed/app.yaml deleted file mode 100644 index 0f248dbb..00000000 --- a/examples/newsfeed/app.yaml +++ /dev/null @@ -1,19 +0,0 @@ -application: facebook-example -version: 1 -runtime: python -api_version: 1 - -handlers: -- url: /static - static_dir: static - -- url: /favicon\.ico - static_files: static/favicon.ico - upload: static/favicon.ico - -- url: /robots\.txt - static_files: static/robots.txt - upload: static/robots.txt - -- url: /.* - script: facebookclient.py diff --git a/examples/newsfeed/facebookclient.py b/examples/newsfeed/facebookclient.py deleted file mode 100644 index 3cdb2597..00000000 --- a/examples/newsfeed/facebookclient.py +++ /dev/null @@ -1,136 +0,0 @@ -#!/usr/bin/env python -# -# Copyright 2010 Facebook -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -"""A Facebook stream client written against the Facebook Graph API.""" - -FACEBOOK_APP_ID = "your app id" -FACEBOOK_APP_SECRET = "your app secret" - -import datetime -import facebook -import os -import os.path -import wsgiref.handlers - -from google.appengine.ext import db -from google.appengine.ext import webapp -from google.appengine.ext.webapp import util -from google.appengine.ext.webapp import template - - -class User(db.Model): - id = db.StringProperty(required=True) - created = db.DateTimeProperty(auto_now_add=True) - updated = db.DateTimeProperty(auto_now=True) - name = db.StringProperty(required=True) - profile_url = db.StringProperty(required=True) - access_token = db.StringProperty(required=True) - - -class BaseHandler(webapp.RequestHandler): - """Provides access to the active Facebook user in self.current_user - - The property is lazy-loaded on first access, using the cookie saved - by the Facebook JavaScript SDK to determine the user ID of the active - user. See http://developers.facebook.com/docs/authentication/ for - more information. - """ - @property - def current_user(self): - """Returns the active user, or None if the user has not logged in.""" - if not hasattr(self, "_current_user"): - self._current_user = None - cookie = facebook.get_user_from_cookie( - self.request.cookies, FACEBOOK_APP_ID, FACEBOOK_APP_SECRET) - if cookie: - # Store a local instance of the user data so we don't need - # a round-trip to Facebook on every request - user = User.get_by_key_name(cookie["uid"]) - if not user: - graph = facebook.GraphAPI(cookie["access_token"]) - profile = graph.get_object("me") - user = User(key_name=str(profile["id"]), - id=str(profile["id"]), - name=profile["name"], - profile_url=profile["link"], - access_token=cookie["access_token"]) - user.put() - elif user.access_token != cookie["access_token"]: - user.access_token = cookie["access_token"] - user.put() - self._current_user = user - return self._current_user - - @property - def graph(self): - """Returns a Graph API client for the current user.""" - if not hasattr(self, "_graph"): - if self.current_user: - self._graph = facebook.GraphAPI(self.current_user.access_token) - else: - self._graph = facebook.GraphAPI() - return self._graph - - def render(self, path, **kwargs): - args = dict(current_user=self.current_user, - facebook_app_id=FACEBOOK_APP_ID) - args.update(kwargs) - path = os.path.join(os.path.dirname(__file__), "templates", path) - self.response.out.write(template.render(path, args)) - - -class HomeHandler(BaseHandler): - def get(self): - if not self.current_user: - self.render("index.html") - return - try: - news_feed = self.graph.get_connections("me", "home") - except facebook.GraphAPIError: - self.render("index.html") - return - except: - news_feed = {"data": []} - for post in news_feed["data"]: - post["created_time"] = datetime.datetime.strptime( - post["created_time"], "%Y-%m-%dT%H:%M:%S+0000") + \ - datetime.timedelta(hours=7) - self.render("home.html", news_feed=news_feed) - - -class PostHandler(BaseHandler): - def post(self): - message = self.request.get("message") - if not self.current_user or not message: - self.redirect("/") - return - try: - self.graph.put_wall_post(message) - except: - pass - self.redirect("/") - - -def main(): - debug = os.environ.get("SERVER_SOFTWARE", "").startswith("Development/") - util.run_wsgi_app(webapp.WSGIApplication([ - (r"/", HomeHandler), - (r"/post", PostHandler), - ], debug=debug)) - - -if __name__ == "__main__": - main() diff --git a/examples/newsfeed/static/base.css b/examples/newsfeed/static/base.css deleted file mode 100644 index 4cb84c1c..00000000 --- a/examples/newsfeed/static/base.css +++ /dev/null @@ -1,162 +0,0 @@ -body { - background: white; - margin: 0; -} - -body, -input, -textarea { - color: #333; - font-family: "Lucida Grande", Tahoma, Verdana, Arial, sans-serif; - font-size: 13px; -} - -a { - color: #3b5998; - text-decoration: none; -} - -a:hover { - text-decoration: underline; -} - -img { - border: 0; -} - -table { - border: 0; - border-collapse: collapse; - border-spacing: 0; -} - -td { - border: 0; - padding: 0; -} - -#promo { - background: #eee; - padding: 8px; - border-bottom: 1px solid #ccc; - color: gray; - text-align: center; -} - -#body { - max-width: 800px; - margin: auto; - padding: 20px; -} - -#header h1 { - margin: 0; - padding: 0; - font-size: 15px; - line-height: 25px; -} - -#header .button { - float: right; -} - -#content { - clear: both; - margin-top: 15px; -} - -.clearfix:after { - clear: both; - content: "."; - display: block; - font-size: 0; - height: 0; - line-height: 0; - visibility: hidden; -} - -.clearfix { - display: block; - zoom: 1; -} - -.feed .entry { - padding-top: 9px; - border-top: 1px solid #eee; - margin-top: 9px; -} - -.feed .entry .profile { - float: left; - line-height: 0; -} - -.feed .entry .profile img { - width: 50px; - height: 50px; -} - -.feed .entry .body { - margin-left: 60px; -} - -.feed .entry .name { - font-weight: bold; -} - -.feed .entry .attachment { - font-size: 11px; - line-height: 15px; - margin-top: 8px; - margin-bottom: 8px; - color: gray; -} - -.feed .entry .attachment.nopicture { - border-left: 2px solid #ccc; - padding-left: 10px; -} - -.feed .entry .attachment .picture { - line-height: 0; - float: left; - padding-right: 10px; -} - -.feed .entry .attachment .picture img { - border: 1px solid #ccc; - padding: 3px; -} - -.feed .entry .attachment .picture a:hover img { - border-color: #3b5998; -} - -.feed .entry .info { - font-size: 11px; - line-height: 17px; - margin-top: 3px; - color: gray; -} - -.feed .entry .info.icon { - background-position: left center; - background-repeat: no-repeat; - padding-left: 20px; -} - -.feed .post .textbox { - margin-right: 6px; -} - -.feed .post .textbox textarea { - margin: 0; - border: 1px solid #bbb; - border-top-color: #aeaeae; - padding: 2px; - width: 100%; -} - -.feed .post .buttons { - text-align: right; -} diff --git a/examples/newsfeed/static/favicon.ico b/examples/newsfeed/static/favicon.ico deleted file mode 100644 index dfcd7079..00000000 Binary files a/examples/newsfeed/static/favicon.ico and /dev/null differ diff --git a/examples/newsfeed/static/robots.txt b/examples/newsfeed/static/robots.txt deleted file mode 100644 index c6742d8a..00000000 --- a/examples/newsfeed/static/robots.txt +++ /dev/null @@ -1,2 +0,0 @@ -User-Agent: * -Disallow: / diff --git a/examples/newsfeed/templates/base.html b/examples/newsfeed/templates/base.html deleted file mode 100644 index bb30ef4f..00000000 --- a/examples/newsfeed/templates/base.html +++ /dev/null @@ -1,29 +0,0 @@ - - - - - {% block title %}Facebook Client Example{% endblock %} - - {% block head %}{% endblock %} - - -
This application is a demo of the Facebook Graph API, the core part of the Facebook Platform. See source code »
-
{% block body %}{% endblock %}
-
- - - diff --git a/examples/newsfeed/templates/home.html b/examples/newsfeed/templates/home.html deleted file mode 100644 index ce40b82c..00000000 --- a/examples/newsfeed/templates/home.html +++ /dev/null @@ -1,52 +0,0 @@ -{% extends "base.html" %} - -{% block body %} - -
- -
-
-
-
-
-
-
-
-
- - {% for post in news_feed.data %} -
-
-
-
- {{ post.from.name|escape }} - {% if post.message %}{{ post.message|escape }}{% endif %} -
- {% if post.caption or post.picture %} -
- {% if post.picture %} -
- {% endif %} - {% if post.name %} - - {% endif %} - {% if post.caption %} -
{{ post.caption|escape }}
- {% endif %} - {% if post.description %} -
{{ post.description|escape }}
- {% endif %} -
- {% endif %} -
- {{ post.created_time|timesince }} ago -
-
-
- {% endfor %} - -
-{% endblock %} diff --git a/examples/newsfeed/templates/index.html b/examples/newsfeed/templates/index.html deleted file mode 100644 index 06be7d31..00000000 --- a/examples/newsfeed/templates/index.html +++ /dev/null @@ -1,7 +0,0 @@ -{% extends "base.html" %} - -{% block body %} -

This application is a simple Facebook client. It shows you your News Feed and enables you to post status messages back to your profile. It is designed to demonstrate the use of the Facebook Graph API, the core part of the Facebook Platform. To get started, log in to Facebook below:

- -

You can download the source code to this application on GitHub.

-{% endblock %} diff --git a/examples/oauth/app.yaml b/examples/oauth/app.yaml deleted file mode 100644 index 7768a409..00000000 --- a/examples/oauth/app.yaml +++ /dev/null @@ -1,8 +0,0 @@ -application: facebook-example -version: 1 -runtime: python -api_version: 1 - -handlers: -- url: /.* - script: facebookoauth.py diff --git a/examples/oauth/facebookoauth.py b/examples/oauth/facebookoauth.py deleted file mode 100644 index cb36b563..00000000 --- a/examples/oauth/facebookoauth.py +++ /dev/null @@ -1,174 +0,0 @@ -#!/usr/bin/env python -# -# Copyright 2010 Facebook -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -"""A barebones AppEngine application that uses Facebook for login. - -This application uses OAuth 2.0 directly rather than relying on Facebook's -JavaScript SDK for login. It also accesses the Facebook Graph API directly -rather than using the Python SDK. It is designed to illustrate how easy -it is to use the Facebook Platform without any third party code. - -See the "appengine" directory for an example using the JavaScript SDK. -Using JavaScript is recommended if it is feasible for your application, -as it handles some complex authentication states that can only be detected -in client-side code. -""" - -FACEBOOK_APP_ID = "your app id" -FACEBOOK_APP_SECRET = "your app secret" - -import base64 -import cgi -import Cookie -import email.utils -import hashlib -import hmac -import logging -import os.path -import time -import urllib -import wsgiref.handlers - -from django.utils import simplejson as json -from google.appengine.ext import db -from google.appengine.ext import webapp -from google.appengine.ext.webapp import util -from google.appengine.ext.webapp import template - - -class User(db.Model): - id = db.StringProperty(required=True) - created = db.DateTimeProperty(auto_now_add=True) - updated = db.DateTimeProperty(auto_now=True) - name = db.StringProperty(required=True) - profile_url = db.StringProperty(required=True) - access_token = db.StringProperty(required=True) - - -class BaseHandler(webapp.RequestHandler): - @property - def current_user(self): - """Returns the logged in Facebook user, or None if unconnected.""" - if not hasattr(self, "_current_user"): - self._current_user = None - user_id = parse_cookie(self.request.cookies.get("fb_user")) - if user_id: - self._current_user = User.get_by_key_name(user_id) - return self._current_user - - -class HomeHandler(BaseHandler): - def get(self): - path = os.path.join(os.path.dirname(__file__), "oauth.html") - args = dict(current_user=self.current_user) - self.response.out.write(template.render(path, args)) - - -class LoginHandler(BaseHandler): - def get(self): - verification_code = self.request.get("code") - args = dict(client_id=FACEBOOK_APP_ID, - redirect_uri=self.request.path_url) - if self.request.get("code"): - args["client_secret"] = FACEBOOK_APP_SECRET - args["code"] = self.request.get("code") - response = cgi.parse_qs(urllib.urlopen( - "https://graph.facebook.com/oauth/access_token?" + - urllib.urlencode(args)).read()) - access_token = response["access_token"][-1] - - # Download the user profile and cache a local instance of the - # basic profile info - profile = json.load(urllib.urlopen( - "https://graph.facebook.com/me?" + - urllib.urlencode(dict(access_token=access_token)))) - user = User(key_name=str(profile["id"]), id=str(profile["id"]), - name=profile["name"], access_token=access_token, - profile_url=profile["link"]) - user.put() - set_cookie(self.response, "fb_user", str(profile["id"]), - expires=time.time() + 30 * 86400) - self.redirect("/") - else: - self.redirect( - "https://graph.facebook.com/oauth/authorize?" + - urllib.urlencode(args)) - - -class LogoutHandler(BaseHandler): - def get(self): - set_cookie(self.response, "fb_user", "", expires=time.time() - 86400) - self.redirect("/") - - -def set_cookie(response, name, value, domain=None, path="/", expires=None): - """Generates and signs a cookie for the give name/value""" - timestamp = str(int(time.time())) - value = base64.b64encode(value) - signature = cookie_signature(value, timestamp) - cookie = Cookie.BaseCookie() - cookie[name] = "|".join([value, timestamp, signature]) - cookie[name]["path"] = path - if domain: - cookie[name]["domain"] = domain - if expires: - cookie[name]["expires"] = email.utils.formatdate( - expires, localtime=False, usegmt=True) - response.headers._headers.append(("Set-Cookie", cookie.output()[12:])) - - -def parse_cookie(value): - """Parses and verifies a cookie value from set_cookie""" - if not value: - return None - parts = value.split("|") - if len(parts) != 3: - return None - if cookie_signature(parts[0], parts[1]) != parts[2]: - logging.warning("Invalid cookie signature %r", value) - return None - timestamp = int(parts[1]) - if timestamp < time.time() - 30 * 86400: - logging.warning("Expired cookie %r", value) - return None - try: - return base64.b64decode(parts[0]).strip() - except: - return None - - -def cookie_signature(*parts): - """Generates a cookie signature. - - We use the Facebook app secret since it is different for every app (so - people using this example don't accidentally all use the same secret). - """ - hash = hmac.new(FACEBOOK_APP_SECRET, digestmod=hashlib.sha1) - for part in parts: - hash.update(part) - return hash.hexdigest() - - -def main(): - util.run_wsgi_app(webapp.WSGIApplication([ - (r"/", HomeHandler), - (r"/auth/login", LoginHandler), - (r"/auth/logout", LogoutHandler), - ])) - - -if __name__ == "__main__": - main() diff --git a/examples/oauth/oauth.html b/examples/oauth/oauth.html deleted file mode 100644 index 0df44dc1..00000000 --- a/examples/oauth/oauth.html +++ /dev/null @@ -1,17 +0,0 @@ - - - - - Facebook OAuth Example - - - {% if current_user %} -

-

You are logged in as {{ current_user.name|escape }}

-

Log out

- {% else %} -

You are not yet logged into this site

-

Log in with Facebook

- {% endif %} - - diff --git a/examples/tornado/example.html b/examples/tornado/example.html index 4693fb1c..10bf3618 100644 --- a/examples/tornado/example.html +++ b/examples/tornado/example.html @@ -8,7 +8,7 @@ {% if current_user %} -

+

Hello, {{ escape(current_user.name) }}

{% end %} @@ -24,7 +24,7 @@ (function() { var e = document.createElement('script'); e.type = 'text/javascript'; - e.src = document.location.protocol + '//connect.facebook.net/en_US/all.js'; + e.src = document.location.protocol + '//connect.facebook.net/en_US/sdk.js'; e.async = true; document.getElementById('fb-root').appendChild(e); }()); diff --git a/examples/tornado/example.py b/examples/tornado/example.py index 6eaa38b1..70201e5a 100755 --- a/examples/tornado/example.py +++ b/examples/tornado/example.py @@ -6,7 +6,7 @@ # not use this file except in compliance with the License. You may obtain # a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT @@ -40,36 +40,47 @@ class BaseHandler(tornado.web.RequestHandler): """Implements authentication via the Facebook JavaScript SDK cookie.""" + def get_current_user(self): cookies = dict((n, self.cookies[n].value) for n in self.cookies.keys()) cookie = facebook.get_user_from_cookie( - cookies, options.facebook_app_id, options.facebook_app_secret) + cookies, options.facebook_app_id, options.facebook_app_secret + ) if not cookie: return None - user = self.db.get( - "SELECT * FROM users WHERE id = %s", cookie["uid"]) + user = self.db.get("SELECT * FROM users WHERE id = %s", cookie["uid"]) if not user: # TODO: Make this fetch async rather than blocking graph = facebook.GraphAPI(cookie["access_token"]) profile = graph.get_object("me") self.db.execute( "REPLACE INTO users (id, name, profile_url, access_token) " - "VALUES (%s,%s,%s,%s)", profile["id"], profile["name"], - profile["link"], cookie["access_token"]) + "VALUES (%s,%s,%s,%s)", + profile["id"], + profile["name"], + profile["link"], + cookie["access_token"], + ) user = self.db.get( - "SELECT * FROM users WHERE id = %s", profile["id"]) + "SELECT * FROM users WHERE id = %s", profile["id"] + ) elif user.access_token != cookie["access_token"]: self.db.execute( "UPDATE users SET access_token = %s WHERE id = %s", - cookie["access_token"], user.id) + cookie["access_token"], + user.id, + ) return user @property def db(self): if not hasattr(BaseHandler, "_db"): BaseHandler._db = tornado.database.Connection( - host=options.mysql_host, database=options.mysql_database, - user=options.mysql_user, password=options.mysql_password) + host=options.mysql_host, + database=options.mysql_database, + user=options.mysql_user, + password=options.mysql_password, + ) return BaseHandler._db @@ -80,9 +91,9 @@ def get(self): def main(): tornado.options.parse_command_line() - http_server = tornado.httpserver.HTTPServer(tornado.web.Application([ - (r"/", MainHandler), - ])) + http_server = tornado.httpserver.HTTPServer( + tornado.web.Application([(r"/", MainHandler)]) + ) http_server.listen(options.port) tornado.ioloop.IOLoop.instance().start() diff --git a/examples/tornado/schema.sql b/examples/tornado/schema.sql index 8af9ff22..c1f7dd7a 100644 --- a/examples/tornado/schema.sql +++ b/examples/tornado/schema.sql @@ -4,7 +4,7 @@ -- not use this file except in compliance with the License. You may obtain -- a copy of the License at -- --- http://www.apache.org/licenses/LICENSE-2.0 +-- https://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, software -- distributed under the License is distributed on an "AS IS" BASIS, WITHOUT diff --git a/facebook.py b/facebook.py deleted file mode 100755 index 4f81773a..00000000 --- a/facebook.py +++ /dev/null @@ -1,556 +0,0 @@ -#!/usr/bin/env python -# -# Copyright 2010 Facebook -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -"""Python client library for the Facebook Platform. - -This client library is designed to support the Graph API and the -official Facebook JavaScript SDK, which is the canonical way to -implement Facebook authentication. Read more about the Graph API at -http://developers.facebook.com/docs/api. You can download the Facebook -JavaScript SDK at http://github.com/facebook/connect-js/. - -If your application is using Google AppEngine's webapp framework, your -usage of this module might look like this: - -user = facebook.get_user_from_cookie(self.request.cookies, key, secret) -if user: - graph = facebook.GraphAPI(user["access_token"]) - profile = graph.get_object("me") - friends = graph.get_connections("me", "friends") - -""" - -import cgi -import time -import urllib -import urllib2 -import httplib -import hashlib -import hmac -import base64 -import logging -import socket - -# Find a JSON parser -try: - import simplejson as json -except ImportError: - try: - from django.utils import simplejson as json - except ImportError: - import json -_parse_json = json.loads - -# Find a query string parser -try: - from urlparse import parse_qs -except ImportError: - from cgi import parse_qs - - -class GraphAPI(object): - """A client for the Facebook Graph API. - - See http://developers.facebook.com/docs/api for complete - documentation for the API. - - The Graph API is made up of the objects in Facebook (e.g., people, - pages, events, photos) and the connections between them (e.g., - friends, photo tags, and event RSVPs). This client provides access - to those primitive types in a generic way. For example, given an - OAuth access token, this will fetch the profile of the active user - and the list of the user's friends: - - graph = facebook.GraphAPI(access_token) - user = graph.get_object("me") - friends = graph.get_connections(user["id"], "friends") - - You can see a list of all of the objects and connections supported - by the API at http://developers.facebook.com/docs/reference/api/. - - You can obtain an access token via OAuth or by using the Facebook - JavaScript SDK. See - http://developers.facebook.com/docs/authentication/ for details. - - If you are using the JavaScript SDK, you can use the - get_user_from_cookie() method below to get the OAuth access token - for the active user from the cookie saved by the SDK. - - """ - def __init__(self, access_token=None, timeout=None): - self.access_token = access_token - self.timeout = timeout - - def get_object(self, id, **args): - """Fetchs the given object from the graph.""" - return self.request(id, args) - - def get_objects(self, ids, **args): - """Fetchs all of the given object from the graph. - - We return a map from ID to object. If any of the IDs are - invalid, we raise an exception. - """ - args["ids"] = ",".join(ids) - return self.request("", args) - - def get_connections(self, id, connection_name, **args): - """Fetchs the connections for given object.""" - return self.request(id + "/" + connection_name, args) - - def put_object(self, parent_object, connection_name, **data): - """Writes the given object to the graph, connected to the given parent. - - For example, - - graph.put_object("me", "feed", message="Hello, world") - - writes "Hello, world" to the active user's wall. Likewise, this - will comment on a the first post of the active user's feed: - - feed = graph.get_connections("me", "feed") - post = feed["data"][0] - graph.put_object(post["id"], "comments", message="First!") - - See http://developers.facebook.com/docs/api#publishing for all - of the supported writeable objects. - - Certain write operations require extended permissions. For - example, publishing to a user's feed requires the - "publish_actions" permission. See - http://developers.facebook.com/docs/publishing/ for details - about publishing permissions. - - """ - assert self.access_token, "Write operations require an access token" - return self.request(parent_object + "/" + connection_name, - post_args=data) - - def put_wall_post(self, message, attachment={}, profile_id="me"): - """Writes a wall post to the given profile's wall. - - We default to writing to the authenticated user's wall if no - profile_id is specified. - - attachment adds a structured attachment to the status message - being posted to the Wall. It should be a dictionary of the form: - - {"name": "Link name" - "link": "http://www.example.com/", - "caption": "{*actor*} posted a new review", - "description": "This is a longer description of the attachment", - "picture": "http://www.example.com/thumbnail.jpg"} - - """ - return self.put_object(profile_id, "feed", message=message, - **attachment) - - def put_comment(self, object_id, message): - """Writes the given comment on the given post.""" - return self.put_object(object_id, "comments", message=message) - - def put_like(self, object_id): - """Likes the given post.""" - return self.put_object(object_id, "likes") - - def delete_object(self, id): - """Deletes the object with the given ID from the graph.""" - self.request(id, post_args={"method": "delete"}) - - def delete_request(self, user_id, request_id): - """Deletes the Request with the given ID for the given user.""" - conn = httplib.HTTPSConnection('graph.facebook.com') - - url = '/%s_%s?%s' % ( - request_id, - user_id, - urllib.urlencode({'access_token': self.access_token}), - ) - conn.request('DELETE', url) - response = conn.getresponse() - data = response.read() - - response = _parse_json(data) - # Raise an error if we got one, but don't not if Facebook just - # gave us a Bool value - if (response and isinstance(response, dict) and response.get("error")): - raise GraphAPIError(response) - - conn.close() - - def put_photo(self, image, message=None, album_id=None, **kwargs): - """Uploads an image using multipart/form-data. - - image=File like object for the image - message=Caption for your image - album_id=None posts to /me/photos which uses or creates and uses - an album for your application. - - """ - object_id = album_id or "me" - #it would have been nice to reuse self.request; - #but multipart is messy in urllib - post_args = { - 'access_token': self.access_token, - 'source': image, - 'message': message, - } - post_args.update(kwargs) - content_type, body = self._encode_multipart_form(post_args) - req = urllib2.Request(("https://graph.facebook.com/%s/photos" % - object_id), - data=body) - req.add_header('Content-Type', content_type) - try: - data = urllib2.urlopen(req).read() - #For Python 3 use this: - #except urllib2.HTTPError as e: - except urllib2.HTTPError, e: - data = e.read() # Facebook sends OAuth errors as 400, and urllib2 - # throws an exception, we want a GraphAPIError - try: - response = _parse_json(data) - # Raise an error if we got one, but don't not if Facebook just - # gave us a Bool value - if (response and isinstance(response, dict) and - response.get("error")): - raise GraphAPIError(response) - except ValueError: - response = data - - return response - - # based on: http://code.activestate.com/recipes/146306/ - def _encode_multipart_form(self, fields): - """Encode files as 'multipart/form-data'. - - Fields are a dict of form name-> value. For files, value should - be a file object. Other file-like objects might work and a fake - name will be chosen. - - Returns (content_type, body) ready for httplib.HTTP instance. - - """ - BOUNDARY = '----------ThIs_Is_tHe_bouNdaRY_$' - CRLF = '\r\n' - L = [] - for (key, value) in fields.items(): - logging.debug("Encoding %s, (%s)%s" % (key, type(value), value)) - if not value: - continue - L.append('--' + BOUNDARY) - if hasattr(value, 'read') and callable(value.read): - filename = getattr(value, 'name', '%s.jpg' % key) - L.append(('Content-Disposition: form-data;' - 'name="%s";' - 'filename="%s"') % (key, filename)) - L.append('Content-Type: image/jpeg') - value = value.read() - logging.debug(type(value)) - else: - L.append('Content-Disposition: form-data; name="%s"' % key) - L.append('') - if isinstance(value, unicode): - logging.debug("Convert to ascii") - value = value.encode('ascii') - L.append(value) - L.append('--' + BOUNDARY + '--') - L.append('') - body = CRLF.join(L) - content_type = 'multipart/form-data; boundary=%s' % BOUNDARY - return content_type, body - - def request(self, path, args=None, post_args=None): - """Fetches the given path in the Graph API. - - We translate args to a valid query string. If post_args is - given, we send a POST request to the given path with the given - arguments. - - """ - args = args or {} - - if self.access_token: - if post_args is not None: - post_args["access_token"] = self.access_token - else: - args["access_token"] = self.access_token - post_data = None if post_args is None else urllib.urlencode(post_args) - try: - file = urllib2.urlopen("https://graph.facebook.com/" + path + "?" + - urllib.urlencode(args), - post_data, timeout=self.timeout) - except urllib2.HTTPError, e: - response = _parse_json(e.read()) - raise GraphAPIError(response) - except TypeError: - # Timeout support for Python <2.6 - if self.timeout: - socket.setdefaulttimeout(self.timeout) - file = urllib2.urlopen("https://graph.facebook.com/" + path + "?" + - urllib.urlencode(args), post_data) - try: - fileInfo = file.info() - if fileInfo.maintype == 'text': - response = _parse_json(file.read()) - elif fileInfo.maintype == 'image': - mimetype = fileInfo['content-type'] - response = { - "data": file.read(), - "mime-type": mimetype, - "url": file.url, - } - else: - raise GraphAPIError('Maintype was not text or image') - finally: - file.close() - if response and isinstance(response, dict) and response.get("error"): - raise GraphAPIError(response["error"]["type"], - response["error"]["message"]) - return response - - def fql(self, query, args=None, post_args=None): - """FQL query. - - Example query: "SELECT affiliations FROM user WHERE uid = me()" - - """ - args = args or {} - if self.access_token: - if post_args is not None: - post_args["access_token"] = self.access_token - else: - args["access_token"] = self.access_token - post_data = None if post_args is None else urllib.urlencode(post_args) - - args["q"] = query - args["format"] = "json" - - try: - file = urllib2.urlopen("https://graph.facebook.com/fql?" + - urllib.urlencode(args), - post_data, timeout=self.timeout) - except TypeError: - # Timeout support for Python <2.6 - if self.timeout: - socket.setdefaulttimeout(self.timeout) - file = urllib2.urlopen("https://graph.facebook.com/fql?" + - urllib.urlencode(args), - post_data) - - try: - content = file.read() - response = _parse_json(content) - #Return a list if success, return a dictionary if failed - if type(response) is dict and "error_code" in response: - raise GraphAPIError(response) - except Exception, e: - raise e - finally: - file.close() - - return response - - def extend_access_token(self, app_id, app_secret): - """ - Extends the expiration time of a valid OAuth access token. See - - - """ - args = { - "client_id": app_id, - "client_secret": app_secret, - "grant_type": "fb_exchange_token", - "fb_exchange_token": self.access_token, - } - response = urllib2.urlopen("https://graph.facebook.com/oauth/" - "access_token?" + - urllib.urlencode(args)).read() - query_str = parse_qs(response) - if "access_token" in query_str: - result = {"access_token": query_str["access_token"][0]} - if "expires" in query_str: - result["expires"] = query_str["expires"][0] - return result - else: - response = json.loads(response) - raise GraphAPIError(response) - - -class GraphAPIError(Exception): - def __init__(self, result): - #Exception.__init__(self, message) - #self.type = type - self.result = result - try: - self.type = result["error_code"] - except: - self.type = "" - - # OAuth 2.0 Draft 10 - try: - self.message = result["error_description"] - except: - # OAuth 2.0 Draft 00 - try: - self.message = result["error"]["message"] - except: - # REST server style - try: - self.message = result["error_msg"] - except: - self.message = result - - Exception.__init__(self, self.message) - - -def get_user_from_cookie(cookies, app_id, app_secret): - """Parses the cookie set by the official Facebook JavaScript SDK. - - cookies should be a dictionary-like object mapping cookie names to - cookie values. - - If the user is logged in via Facebook, we return a dictionary with - the keys "uid" and "access_token". The former is the user's - Facebook ID, and the latter can be used to make authenticated - requests to the Graph API. If the user is not logged in, we - return None. - - Download the official Facebook JavaScript SDK at - http://github.com/facebook/connect-js/. Read more about Facebook - authentication at - http://developers.facebook.com/docs/authentication/. - - """ - cookie = cookies.get("fbsr_" + app_id, "") - if not cookie: - return None - parsed_request = parse_signed_request(cookie, app_secret) - if not parsed_request: - return None - try: - result = get_access_token_from_code(parsed_request["code"], "", - app_id, app_secret) - except GraphAPIError: - return None - result["uid"] = parsed_request["user_id"] - return result - - -def parse_signed_request(signed_request, app_secret): - """ Return dictionary with signed request data. - - We return a dictionary containing the information in the - signed_request. This includes a user_id if the user has authorised - your application, as well as any information requested. - - If the signed_request is malformed or corrupted, False is returned. - - """ - try: - encoded_sig, payload = map(str, signed_request.split('.', 1)) - - sig = base64.urlsafe_b64decode(encoded_sig + "=" * - ((4 - len(encoded_sig) % 4) % 4)) - data = base64.urlsafe_b64decode(payload + "=" * - ((4 - len(payload) % 4) % 4)) - except IndexError: - # Signed request was malformed. - return False - except TypeError: - # Signed request had a corrupted payload. - return False - - data = _parse_json(data) - if data.get('algorithm', '').upper() != 'HMAC-SHA256': - return False - - # HMAC can only handle ascii (byte) strings - # http://bugs.python.org/issue5285 - app_secret = app_secret.encode('ascii') - payload = payload.encode('ascii') - - expected_sig = hmac.new(app_secret, - msg=payload, - digestmod=hashlib.sha256).digest() - if sig != expected_sig: - return False - - return data - - -def auth_url(app_id, canvas_url, perms=None, **kwargs): - url = "https://www.facebook.com/dialog/oauth?" - kvps = {'client_id': app_id, 'redirect_uri': canvas_url} - if perms: - kvps['scope'] = ",".join(perms) - kvps.update(kwargs) - return url + urllib.urlencode(kvps) - -def get_access_token_from_code(code, redirect_uri, app_id, app_secret): - """Get an access token from the "code" returned from an OAuth dialog. - - Returns a dict containing the user-specific access token and its - expiration date (if applicable). - - """ - args = { - "code": code, - "redirect_uri": redirect_uri, - "client_id": app_id, - "client_secret": app_secret, - } - # We would use GraphAPI.request() here, except for that the fact - # that the response is a key-value pair, and not JSON. - response = urllib2.urlopen("https://graph.facebook.com/oauth/access_token" + - "?" + urllib.urlencode(args)).read() - query_str = parse_qs(response) - if "access_token" in query_str: - result = {"access_token": query_str["access_token"][0]} - if "expires" in query_str: - result["expires"] = query_str["expires"][0] - return result - else: - response = json.loads(response) - raise GraphAPIError(response) - - -def get_app_access_token(app_id, app_secret): - """Get the access_token for the app. - - This token can be used for insights and creating test users. - - app_id = retrieved from the developer page - app_secret = retrieved from the developer page - - Returns the application access_token. - - """ - # Get an app access token - args = {'grant_type': 'client_credentials', - 'client_id': app_id, - 'client_secret': app_secret} - - file = urllib2.urlopen("https://graph.facebook.com/oauth/access_token?" + - urllib.urlencode(args)) - - try: - result = file.read().split("=")[1] - finally: - file.close() - - return result diff --git a/facebook/__init__.py b/facebook/__init__.py new file mode 100755 index 00000000..e91c5939 --- /dev/null +++ b/facebook/__init__.py @@ -0,0 +1,522 @@ +#!/usr/bin/env python +# +# Copyright 2010 Facebook +# Copyright 2015 Mobolic +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Python client library for the Facebook Platform. + +This client library is designed to support the Graph API and the +official Facebook JavaScript SDK, which is the canonical way to +implement Facebook authentication. Read more about the Graph API at +https://developers.facebook.com/docs/graph-api. + +""" + +import hashlib +import hmac +import binascii +import base64 +import requests +import json +import re +from urllib.parse import parse_qs, urlencode, urlparse + +from . import version + + +__version__ = version.__version__ + +FACEBOOK_GRAPH_URL = "https://graph.facebook.com/" +FACEBOOK_WWW_URL = "https://www.facebook.com/" +FACEBOOK_OAUTH_DIALOG_PATH = "dialog/oauth?" +VALID_API_VERSIONS = ["3.1", "3.2", "3.3", "4.0", "5.0", "6.0", "7.0", "8.0"] +VALID_SEARCH_TYPES = ["place", "placetopic"] + + +class GraphAPI(object): + """A client for the Facebook Graph API. + + https://developers.facebook.com/docs/graph-api + + The Graph API is made up of the objects in Facebook (e.g., people, + pages, events, photos) and the connections between them (e.g., + friends, photo tags, and event RSVPs). This client provides access + to those primitive types in a generic way. For example, given an + OAuth access token, this will fetch the profile of the active user + and the list of the user's friends: + + graph = facebook.GraphAPI(access_token) + user = graph.get_object("me") + friends = graph.get_connections(user["id"], "friends") + + You can see a list of all of the objects and connections supported + by the API at https://developers.facebook.com/docs/graph-api/reference/. + + You can obtain an access token via OAuth or by using the Facebook + JavaScript SDK. See + https://developers.facebook.com/docs/facebook-login for details. + + If you are using the JavaScript SDK, you can use the + get_user_from_cookie() method below to get the OAuth access token + for the active user from the cookie saved by the SDK. + + """ + + def __init__( + self, + access_token=None, + timeout=None, + version=None, + proxies=None, + session=None, + app_secret=None, + ): + # The default version is only used if the version kwarg does not exist. + default_version = VALID_API_VERSIONS[0] + + self.access_token = access_token + self.timeout = timeout + self.proxies = proxies + self.session = session or requests.Session() + self.app_secret_hmac = None + + if version: + version_regex = re.compile(r"^\d\.\d{1,2}$") + match = version_regex.search(str(version)) + if match is not None: + if str(version) not in VALID_API_VERSIONS: + raise GraphAPIError( + "Valid API versions are " + + str(VALID_API_VERSIONS).strip("[]") + ) + else: + self.version = "v" + str(version) + else: + raise GraphAPIError( + "Version number should be in the" + " following format: #.# (e.g. 2.0)." + ) + else: + self.version = "v" + default_version + + if app_secret and access_token: + self.app_secret_hmac = hmac.new( + app_secret.encode("ascii"), + msg=access_token.encode("ascii"), + digestmod=hashlib.sha256, + ).hexdigest() + + def get_permissions(self, user_id): + """Fetches the permissions object from the graph.""" + response = self.request( + "{0}/{1}/permissions".format(self.version, user_id), {} + )["data"] + return {x["permission"] for x in response if x["status"] == "granted"} + + def get_object(self, id, **args): + """Fetches the given object from the graph.""" + return self.request("{0}/{1}".format(self.version, id), args) + + def get_objects(self, ids, **args): + """Fetches all of the given object from the graph. + + We return a map from ID to object. If any of the IDs are + invalid, we raise an exception. + """ + args["ids"] = ",".join(ids) + return self.request(self.version + "/", args) + + def search(self, type, **args): + """https://developers.facebook.com/docs/places/search""" + if type not in VALID_SEARCH_TYPES: + raise GraphAPIError( + "Valid types are: %s" % ", ".join(VALID_SEARCH_TYPES) + ) + + args["type"] = type + return self.request(self.version + "/search/", args) + + def get_connections(self, id, connection_name, **args): + """Fetches the connections for given object.""" + return self.request( + "{0}/{1}/{2}".format(self.version, id, connection_name), args + ) + + def get_all_connections(self, id, connection_name, **args): + """Get all pages from a get_connections call + + This will iterate over all pages returned by a get_connections call + and yield the individual items. + """ + while True: + page = self.get_connections(id, connection_name, **args) + for post in page["data"]: + yield post + next = page.get("paging", {}).get("next") + if not next: + return + args = parse_qs(urlparse(next).query) + del args["access_token"] + + def put_object(self, parent_object, connection_name, **data): + """Writes the given object to the graph, connected to the given parent. + + For example, + + graph.put_object("me", "feed", message="Hello, world") + + writes "Hello, world" to the active user's wall. Likewise, this + will comment on the first post of the active user's feed: + + feed = graph.get_connections("me", "feed") + post = feed["data"][0] + graph.put_object(post["id"], "comments", message="First!") + + Certain operations require extended permissions. See + https://developers.facebook.com/docs/facebook-login/permissions + for details about permissions. + + """ + assert self.access_token, "Write operations require an access token" + return self.request( + "{0}/{1}/{2}".format(self.version, parent_object, connection_name), + post_args=data, + method="POST", + ) + + def put_comment(self, object_id, message): + """Writes the given comment on the given post.""" + return self.put_object(object_id, "comments", message=message) + + def put_like(self, object_id): + """Likes the given post.""" + return self.put_object(object_id, "likes") + + def delete_object(self, id): + """Deletes the object with the given ID from the graph.""" + return self.request( + "{0}/{1}".format(self.version, id), method="DELETE" + ) + + def delete_request(self, user_id, request_id): + """Deletes the Request with the given ID for the given user.""" + return self.request( + "{0}_{1}".format(request_id, user_id), method="DELETE" + ) + + def put_photo(self, image, album_path="me/photos", **kwargs): + """ + Upload an image using multipart/form-data. + + image - A file object representing the image to be uploaded. + album_path - A path representing where the image should be uploaded. + + """ + return self.request( + "{0}/{1}".format(self.version, album_path), + post_args=kwargs, + files={"source": image}, + method="POST", + ) + + def get_version(self): + """Fetches the current version number of the Graph API being used.""" + args = {"access_token": self.access_token} + try: + response = self.session.request( + "GET", + FACEBOOK_GRAPH_URL + self.version + "/me", + params=args, + timeout=self.timeout, + proxies=self.proxies, + ) + except requests.HTTPError as e: + response = json.loads(e.read()) + raise GraphAPIError(response) + + try: + headers = response.headers + version = headers["facebook-api-version"].replace("v", "") + return str(version) + except Exception: + raise GraphAPIError("API version number not available") + + def request( + self, path, args=None, post_args=None, files=None, method=None + ): + """Fetches the given path in the Graph API. + + We translate args to a valid query string. If post_args is + given, we send a POST request to the given path with the given + arguments. + + """ + if args is None: + args = dict() + if post_args is not None: + method = "POST" + + # Add `access_token` and app secret proof (`app_secret_hmac`) to + # post_args or args if they exist and have not already been included. + def _add_to_post_args_or_args(arg_name, arg_value): + # If post_args exists, we assume that args either does not exists + # or it does not need updating. + if post_args and arg_name not in post_args: + post_args[arg_name] = arg_value + elif arg_name not in args: + args[arg_name] = arg_value + + if self.access_token: + _add_to_post_args_or_args("access_token", self.access_token) + if self.app_secret_hmac: + _add_to_post_args_or_args("appsecret_proof", self.app_secret_hmac) + + try: + response = self.session.request( + method or "GET", + FACEBOOK_GRAPH_URL + path, + timeout=self.timeout, + params=args, + data=post_args, + proxies=self.proxies, + files=files, + ) + except requests.HTTPError as e: + response = json.loads(e.read()) + raise GraphAPIError(response) + + headers = response.headers + if "json" in headers["content-type"]: + result = response.json() + elif "image/" in headers["content-type"]: + mimetype = headers["content-type"] + result = { + "data": response.content, + "mime-type": mimetype, + "url": response.url, + } + elif "access_token" in parse_qs(response.text): + query_str = parse_qs(response.text) + if "access_token" in query_str: + result = {"access_token": query_str["access_token"][0]} + if "expires" in query_str: + result["expires"] = query_str["expires"][0] + else: + raise GraphAPIError(response.json()) + else: + raise GraphAPIError("Maintype was not text, image, or querystring") + + if result and isinstance(result, dict) and result.get("error"): + raise GraphAPIError(result) + return result + + def get_app_access_token(self, app_id, app_secret, offline=False): + """ + Get the application's access token as a string. + If offline=True, use the concatenated app ID and secret + instead of making an API call. + + """ + if offline: + return "{0}|{1}".format(app_id, app_secret) + else: + args = { + "grant_type": "client_credentials", + "client_id": app_id, + "client_secret": app_secret, + } + + return self.request( + "{0}/oauth/access_token".format(self.version), args=args + )["access_token"] + + def get_access_token_from_code( + self, code, redirect_uri, app_id, app_secret + ): + """Get an access token from the "code" returned from an OAuth dialog. + + Returns a dict containing the user-specific access token and its + expiration date (if applicable). + + """ + args = { + "code": code, + "redirect_uri": redirect_uri, + "client_id": app_id, + "client_secret": app_secret, + } + + return self.request( + "{0}/oauth/access_token".format(self.version), args + ) + + def extend_access_token(self, app_id, app_secret): + """ + Extends the expiration time of a valid OAuth access token. See + + + """ + args = { + "client_id": app_id, + "client_secret": app_secret, + "grant_type": "fb_exchange_token", + "fb_exchange_token": self.access_token, + } + + return self.request( + "{0}/oauth/access_token".format(self.version), args=args + ) + + def debug_access_token(self, token, app_id, app_secret): + """ + Gets information about a user access token issued by an app. See + + + We can generate the app access token by concatenating the app + id and secret: + + """ + args = { + "input_token": token, + "access_token": "{0}|{1}".format(app_id, app_secret), + } + return self.request(self.version + "/" + "debug_token", args=args) + + def get_auth_url(self, app_id, canvas_url, perms=None, **kwargs): + """Build a URL to create an OAuth dialog.""" + url = "{0}{1}/{2}".format( + FACEBOOK_WWW_URL, self.version, FACEBOOK_OAUTH_DIALOG_PATH + ) + + args = {"client_id": app_id, "redirect_uri": canvas_url} + if perms: + args["scope"] = ",".join(perms) + args.update(kwargs) + return url + urlencode(args) + + +class GraphAPIError(Exception): + def __init__(self, result): + self.result = result + self.code = None + self.error_subcode = None + + try: + self.type = result["error_code"] + except (KeyError, TypeError): + self.type = "" + + # OAuth 2.0 Draft 10 + try: + self.message = result["error_description"] + except (KeyError, TypeError): + # OAuth 2.0 Draft 00 + try: + self.message = result["error"]["message"] + self.code = result["error"].get("code") + self.error_subcode = result["error"].get("error_subcode") + if not self.type: + self.type = result["error"].get("type", "") + except (KeyError, TypeError): + # REST server style + try: + self.message = result["error_msg"] + except (KeyError, TypeError): + self.message = result + + Exception.__init__(self, self.message) + + +def get_user_from_cookie(cookies, app_id, app_secret): + """Parses the cookie set by the official Facebook JavaScript SDK. + + cookies should be a dictionary-like object mapping cookie names to + cookie values. + + If the user is logged in via Facebook, we return a dictionary with + the keys "uid" and "access_token". The former is the user's + Facebook ID, and the latter can be used to make authenticated + requests to the Graph API. If the user is not logged in, we + return None. + + Read more about Facebook authentication at + https://developers.facebook.com/docs/facebook-login. + + """ + cookie = cookies.get("fbsr_" + app_id, "") + if not cookie: + return None + parsed_request = parse_signed_request(cookie, app_secret) + if not parsed_request: + return None + try: + result = GraphAPI().get_access_token_from_code( + parsed_request["code"], "", app_id, app_secret + ) + except GraphAPIError: + return None + result["uid"] = parsed_request["user_id"] + return result + + +def parse_signed_request(signed_request, app_secret): + """Return dictionary with signed request data. + + We return a dictionary containing the information in the + signed_request. This includes a user_id if the user has authorised + your application, as well as any information requested. + + If the signed_request is malformed or corrupted, False is returned. + + """ + try: + encoded_sig, payload = map(str, signed_request.split(".", 1)) + + sig = base64.urlsafe_b64decode( + encoded_sig + "=" * ((4 - len(encoded_sig) % 4) % 4) + ) + data = base64.urlsafe_b64decode( + payload + "=" * ((4 - len(payload) % 4) % 4) + ) + except IndexError: + # Signed request was malformed. + return False + except TypeError: + # Signed request had a corrupted payload. + return False + except binascii.Error: + # Signed request had a corrupted payload. + return False + + data = json.loads(data.decode("ascii")) + if data.get("algorithm", "").upper() != "HMAC-SHA256": + return False + + # HMAC can only handle ascii (byte) strings + # https://bugs.python.org/issue5285 + app_secret = app_secret.encode("ascii") + payload = payload.encode("ascii") + + expected_sig = hmac.new( + app_secret, msg=payload, digestmod=hashlib.sha256 + ).digest() + if sig != expected_sig: + return False + + return data diff --git a/facebook/version.py b/facebook/version.py new file mode 100644 index 00000000..f87bac4d --- /dev/null +++ b/facebook/version.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python +# +# Copyright 2015-2018 Mobolic +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +__version__ = "4.0.0-pre" diff --git a/setup.py b/setup.py index f10951d9..13e8373d 100644 --- a/setup.py +++ b/setup.py @@ -1,22 +1,30 @@ #!/usr/bin/env python -from distutils.core import setup +from setuptools import setup + +exec(open("facebook/version.py").read()) setup( name='facebook-sdk', - version='0.4.0', + version=__version__, # noqa: F821 description='This client library is designed to support the Facebook ' 'Graph API and the official Facebook JavaScript SDK, which ' 'is the canonical way to implement Facebook authentication.', author='Facebook', maintainer='Martey Dodoo', - maintainer_email='facebook-sdk@marteydodoo.com', - url='https://github.com/pythonforfacebook/facebook-sdk', + maintainer_email='martey+facebook-sdk@mobolic.com', + url='https://github.com/mobolic/facebook-sdk', license='Apache', - py_modules=[ - 'facebook', - ], + packages=["facebook"], long_description=open("README.rst").read(), classifiers=[ 'License :: OSI Approved :: Apache Software License', + 'Programming Language :: Python', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', ], + install_requires=['requests'], + tests_require=["coverage"], ) diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 00000000..c03946f0 --- /dev/null +++ b/test/__init__.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python +# +# Copyright 2015-2019 Mobolic +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +import os +import unittest + +import facebook + + +class FacebookTestCase(unittest.TestCase): + """ + Sets up application ID and secret from environment and initialises an + empty list for test users. + + """ + + def setUp(self): + try: + self.app_id = os.environ["FACEBOOK_APP_ID"] + self.secret = os.environ["FACEBOOK_SECRET"] + except KeyError: + raise Exception( + "FACEBOOK_APP_ID and FACEBOOK_SECRET " + "must be set as environmental variables." + ) + + self.test_users = [] + + def tearDown(self): + """Deletes the test users included in the test user list.""" + token = facebook.GraphAPI().get_app_access_token( + self.app_id, self.secret, True + ) + graph = facebook.GraphAPI(token) + + for user in self.test_users: + graph.request(user["id"], {}, None, method="DELETE") + del self.test_users[:] + + def assert_raises_multi_regex( + self, + expected_exception, + expected_regexp, + callable_obj=None, + *args, + **kwargs + ): + """ + Custom function to backport assertRaisesRegexp to all supported + versions of Python. + + """ + self.assertRaises(expected_exception, callable_obj, *args, **kwargs) + try: + callable_obj(*args) + except facebook.GraphAPIError as error: + self.assertEqual(error.message, expected_regexp) + + def create_test_users(self, app_id, graph, amount): + """Function for creating test users.""" + for i in range(amount): + u = graph.request( + app_id + "/accounts/test-users", {}, {}, method="POST" + ) + self.test_users.append(u) + + def create_friend_connections(self, user, friends): + """Function for creating friend connections for a test user.""" + user_graph = facebook.GraphAPI(user["access_token"]) + + for friend in friends: + if user["id"] == friend["id"]: + continue + user_graph.request( + user["id"] + "/friends/" + friend["id"], {}, {}, method="POST" + ) + respondent_graph = facebook.GraphAPI(friend["access_token"]) + respondent_graph.request( + friend["id"] + "/friends/" + user["id"], {}, {}, method="POST" + ) diff --git a/test/test_access_tokens.py b/test/test_access_tokens.py new file mode 100644 index 00000000..6d92f0a2 --- /dev/null +++ b/test/test_access_tokens.py @@ -0,0 +1,88 @@ +import facebook +from . import FacebookTestCase + + +class FacebookAccessTokenTestCase(FacebookTestCase): + def test_extend_access_token(self): + """ + Test if extend_access_token requests the correct endpoint. + + Note that this only tests whether extend_access_token returns the + correct error message when called without a proper user-access token. + + """ + try: + facebook.GraphAPI().extend_access_token(self.app_id, self.secret) + except facebook.GraphAPIError as e: + self.assertEqual( + e.message, "fb_exchange_token parameter not specified" + ) + + def test_bogus_access_token(self): + graph = facebook.GraphAPI(access_token="wrong_token") + self.assertRaises(facebook.GraphAPIError, graph.get_object, "me") + + def test_access_with_expired_access_token(self): + expired_token = ( + "AAABrFmeaJjgBAIshbq5ZBqZBICsmveZCZBi6O4w9HSTkFI73VMtmkL9jLuWs" + "ZBZC9QMHvJFtSulZAqonZBRIByzGooCZC8DWr0t1M4BL9FARdQwPWPnIqCiFQ" + ) + graph = facebook.GraphAPI(access_token=expired_token) + self.assertRaises(facebook.GraphAPIError, graph.get_object, "me") + + def test_request_access_tokens_are_unique_to_instances(self): + """Verify that access tokens are unique to each GraphAPI object.""" + graph1 = facebook.GraphAPI(access_token="foo") + graph2 = facebook.GraphAPI(access_token="bar") + # We use `delete_object` so that the access_token will appear + # in request.__defaults__. + try: + graph1.delete_object("baz") + except facebook.GraphAPIError: + pass + try: + graph2.delete_object("baz") + except facebook.GraphAPIError: + pass + self.assertEqual(graph1.request.__defaults__[0], None) + self.assertEqual(graph2.request.__defaults__[0], None) + + +class FacebookAppAccessTokenCase(FacebookTestCase): + """ + Test if application access token is returned properly. + + Note that this only tests if the returned token is a string, not + whether it is valid. + + """ + + def test_get_app_access_token(self): + token = facebook.GraphAPI().get_app_access_token( + self.app_id, self.secret, False + ) + # Since "unicode" does not exist in Python 3, we cannot check + # the following line with flake8 (hence the noqa comment). + assert isinstance(token, str) or isinstance(token, unicode) # noqa + + def test_get_offline_app_access_token(self): + """Verify that offline generation of app access tokens works.""" + token = facebook.GraphAPI().get_app_access_token( + self.app_id, self.secret, offline=True + ) + self.assertEqual(token, "{0}|{1}".format(self.app_id, self.secret)) + + def test_get_deleted_app_access_token(self): + deleted_app_id = "174236045938435" + deleted_secret = "0073dce2d95c4a5c2922d1827ea0cca6" + deleted_error_message = ( + "Error validating application. Application has been deleted." + ) + + self.assert_raises_multi_regex( + facebook.GraphAPIError, + deleted_error_message, + facebook.GraphAPI().get_app_access_token, + deleted_app_id, + deleted_secret, + ) diff --git a/test/test_application_secret_proof.py b/test/test_application_secret_proof.py new file mode 100644 index 00000000..9e489deb --- /dev/null +++ b/test/test_application_secret_proof.py @@ -0,0 +1,116 @@ +from unittest import mock + +import facebook +from . import FacebookTestCase + + +class FacebookAppSecretProofTestCase(FacebookTestCase): + """Tests related to application secret proofs.""" + + PROOF = "4dad02ff1693df832f9c183fe400fc4f601360be06514acb4a73edb783eec345" + + ACCESS_TOKEN = "abc123" + APP_SECRET = "xyz789" + + def test_appsecret_proof_set(self): + """ + Verify that application secret proof is set when a GraphAPI object is + initialized with an application secret and access token. + """ + api = facebook.GraphAPI( + access_token=self.ACCESS_TOKEN, app_secret=self.APP_SECRET + ) + self.assertEqual(api.app_secret_hmac, self.PROOF) + + def test_appsecret_proof_no_access_token(self): + """ + Verify that no application secret proof is set when + a GraphAPI object is initialized with an application secret + and no access token. + """ + api = facebook.GraphAPI(app_secret=self.APP_SECRET) + self.assertEqual(api.app_secret_hmac, None) + + def test_appsecret_proof_no_app_secret(self): + """ + Verify that no application secret proof is set when + a GraphAPI object is initialized with no application secret + and no access token. + """ + api = facebook.GraphAPI(access_token=self.ACCESS_TOKEN) + self.assertEqual(api.app_secret_hmac, None) + + @mock.patch("requests.request") + def test_appsecret_proof_is_set_on_get_request(self, mock_request): + """ + Verify that no application secret proof is sent with + GET requests whena GraphAPI object is initialized + with an application secret and an access token. + """ + api = facebook.GraphAPI( + access_token=self.ACCESS_TOKEN, app_secret=self.APP_SECRET + ) + mock_response = mock.Mock() + mock_response.headers = {"content-type": "json"} + mock_response.json.return_value = {} + mock_request.return_value = mock_response + api.session.request = mock_request + api.request("some-path") + mock_request.assert_called_once_with( + "GET", + "https://graph.facebook.com/some-path", + data=None, + files=None, + params={"access_token": "abc123", "appsecret_proof": self.PROOF}, + proxies=None, + timeout=None, + ) + + @mock.patch("requests.request") + def test_appsecret_proof_is_set_on_post_request(self, mock_request): + """ + Verify that no application secret proof is sent with + POST requests when a GraphAPI object is initialized + with an application secret and an access token. + """ + api = facebook.GraphAPI( + access_token=self.ACCESS_TOKEN, app_secret=self.APP_SECRET + ) + mock_response = mock.Mock() + mock_response.headers = {"content-type": "json"} + mock_response.json.return_value = {} + mock_request.return_value = mock_response + api.session.request = mock_request + api.request("some-path", method="POST") + mock_request.assert_called_once_with( + "POST", + "https://graph.facebook.com/some-path", + data=None, + files=None, + params={"access_token": "abc123", "appsecret_proof": self.PROOF}, + proxies=None, + timeout=None, + ) + + @mock.patch("requests.request") + def test_missing_appsecret_proof_is_not_set_on_request(self, mock_request): + """ + Verify that no application secret proof is set if GraphAPI + object is initialized without an application secret. + """ + api = facebook.GraphAPI(access_token=self.ACCESS_TOKEN) + mock_response = mock.Mock() + mock_response.headers = {"content-type": "json"} + mock_response.json.return_value = {} + mock_request.return_value = mock_response + api.session.request = mock_request + api.request("some-path") + mock_request.assert_called_once_with( + "GET", + "https://graph.facebook.com/some-path", + data=None, + files=None, + params={"access_token": "abc123"}, + proxies=None, + timeout=None, + ) diff --git a/test/test_connections.py b/test/test_connections.py new file mode 100644 index 00000000..d70e3994 --- /dev/null +++ b/test/test_connections.py @@ -0,0 +1,41 @@ +import inspect + +import facebook +from . import FacebookTestCase + + +class FacebookAllConnectionsMethodTestCase(FacebookTestCase): + def test_function_with_zero_connections(self): + token = facebook.GraphAPI().get_app_access_token( + self.app_id, self.secret, True + ) + graph = facebook.GraphAPI(token) + + self.create_test_users(self.app_id, graph, 1) + friends = graph.get_all_connections( + self.test_users[0]["id"], "friends" + ) + + self.assertTrue(inspect.isgenerator(friends)) + self.assertTrue(len(list(friends)) == 0) + + # def test_function_returns_correct_connections(self): + # token = facebook.GraphAPI().get_app_access_token( + # self.app_id, self.secret, True + # ) + # graph = facebook.GraphAPI(token) + + # self.create_test_users(self.app_id, graph, 3) + # self.create_friend_connections(self.test_users[0], self.test_users) + + # friends = graph.get_all_connections( + # self.test_users[0]["id"], "friends" + # ) + # self.assertTrue(inspect.isgenerator(friends)) + + # friends_list = list(friends) + # self.assertTrue(len(friends_list) == 2) + # for f in friends: + # self.assertTrue(isinstance(f, dict)) + # self.assertTrue("name" in f) + # self.assertTrue("id" in f) diff --git a/test/test_graphapierror.py b/test/test_graphapierror.py new file mode 100644 index 00000000..2dc5ce85 --- /dev/null +++ b/test/test_graphapierror.py @@ -0,0 +1,31 @@ +import random +import string +import unittest + +from facebook import GraphAPIError + + +class FacebookTestCase(unittest.TestCase): + """Automated test cases specifically relating to GraphAPIError object.""" + + def test_default_error_subcode(self): + """Verify that default error subcode is None.""" + error = GraphAPIError(None) + self.assertEqual(error.error_subcode, None) + + def test_setting_error_subcode(self): + """Verify that error subcode is set properly.""" + # Generate random string. + error_subcode = "".join( + random.choice(string.ascii_letters + string.digits) + for _ in range(10) + ) + result = { + "error": { + "message": "", + "code": "", + "error_subcode": error_subcode, + } + } + error = GraphAPIError(result) + self.assertEqual(error.error_subcode, error_subcode) diff --git a/test/test_oauth_dialog_url.py b/test/test_oauth_dialog_url.py new file mode 100644 index 00000000..9b6ea603 --- /dev/null +++ b/test/test_oauth_dialog_url.py @@ -0,0 +1,41 @@ +from urllib.parse import parse_qs, urlencode, urlparse + +import facebook +from . import FacebookTestCase + + +class FacebookAuthURLTestCase(FacebookTestCase): + def test_auth_url(self): + graph = facebook.GraphAPI() + perms = ["email", "birthday"] + redirect_url = "https://localhost/facebook/callback/" + + encoded_args = urlencode( + dict( + client_id=self.app_id, + redirect_uri=redirect_url, + scope=",".join(perms), + ) + ) + expected_url = "{0}{1}/{2}{3}".format( + facebook.FACEBOOK_WWW_URL, + graph.version, + facebook.FACEBOOK_OAUTH_DIALOG_PATH, + encoded_args, + ) + + actual_url = graph.get_auth_url(self.app_id, redirect_url, perms=perms) + + # Since the order of the query string parameters might be + # different in each URL, we cannot just compare them to each + # other. + expected_url_result = urlparse(expected_url) + actual_url_result = urlparse(actual_url) + expected_query = parse_qs(expected_url_result.query) + actual_query = parse_qs(actual_url_result.query) + + self.assertEqual(actual_url_result.scheme, expected_url_result.scheme) + self.assertEqual(actual_url_result.netloc, expected_url_result.netloc) + self.assertEqual(actual_url_result.path, expected_url_result.path) + self.assertEqual(actual_url_result.params, expected_url_result.params) + self.assertEqual(actual_query, expected_query) diff --git a/test/test_permissions.py b/test/test_permissions.py new file mode 100644 index 00000000..eb3aaf53 --- /dev/null +++ b/test/test_permissions.py @@ -0,0 +1,32 @@ +import facebook +from . import FacebookTestCase + + +class FacebookUserPermissionsTestCase(FacebookTestCase): + """ + Test if user permissions are retrieved correctly. + + Note that this only tests if the returned JSON object exists and is + structured as expected, not whether any specific scope is included + (other than the default `public_profile` scope). + + """ + + def test_get_user_permissions_node(self): + token = facebook.GraphAPI().get_app_access_token( + self.app_id, self.secret, True + ) + graph = facebook.GraphAPI(access_token=token) + self.create_test_users(self.app_id, graph, 1) + permissions = graph.get_permissions(self.test_users[0]["id"]) + self.assertIsNotNone(permissions) + self.assertTrue("public_profile" in permissions) + self.assertTrue("user_friends" in permissions) + self.assertFalse("email" in permissions) + + def test_get_user_permissions_nonexistant_user(self): + token = facebook.GraphAPI().get_app_access_token( + self.app_id, self.secret, True + ) + with self.assertRaises(facebook.GraphAPIError): + facebook.GraphAPI(token).get_permissions(1) diff --git a/test/test_search.py b/test/test_search.py new file mode 100644 index 00000000..e56c7f33 --- /dev/null +++ b/test/test_search.py @@ -0,0 +1,28 @@ +import facebook +from . import FacebookTestCase + + +class FacebookSearchTestCase(FacebookTestCase): + def setUp(self): + """Create GraphAPI object that can search (i.e. has user token).""" + super(FacebookSearchTestCase, self).setUp() + # Create an app access token to create a test user + # to create a GraphAPI object with a valid user access token. + app_token = facebook.GraphAPI().get_app_access_token( + self.app_id, self.secret, True + ) + self.create_test_users(self.app_id, facebook.GraphAPI(app_token), 1) + user = self.test_users[0] + self.graph = facebook.GraphAPI(user["access_token"]) + + # def test_valid_search_types(self): + # """Verify that search method accepts all valid search types.""" + # for search_type in facebook.VALID_SEARCH_TYPES: + # self.graph.search(type=search_type, q="foobar") + + def test_invalid_search_type(self): + """Verify that search method fails when an invalid type is passed.""" + search_args = {"type": "foo", "q": "bar"} + self.assertRaises( + facebook.GraphAPIError, self.graph.search, search_args + ) diff --git a/test/test_signed_request.py b/test/test_signed_request.py new file mode 100644 index 00000000..c0393486 --- /dev/null +++ b/test/test_signed_request.py @@ -0,0 +1,35 @@ +import facebook +from . import FacebookTestCase + + +class FacebookParseSignedRequestTestCase(FacebookTestCase): + cookie = ( + "Z6pnNcY-TePEBA7IfKta6ipLgrig53M7DRGisKSybBQ." + "eyJhbGdvcml0aG0iOiJITUFDLVNIQTI1NiIsImNvZGUiOiJBUURjSXQ2YnhZ" + "M090T3BSRGtpT1k4UDNlOWgwYzZRNFFuMEFFQnVqR1M3ZEV5LXNtbUt5b3pD" + "dHdhZy1kRmVYNmRUbi12dVBfQVNtek5RbjlkakloZHJIa0VBMHlLMm16T0Ji" + "RS1memVoNUh0Vk5UbnpQUDV3Z2VmUkF1bjhvTkQ4S3I3aUd2a3A4Q2EzODJL" + "NWtqcVl1Z19QV1NUREhqMlY3T2NWaE1GQ2wyWkN2MFk5NnlLUDhfSVAtbnNL" + "b09kcFVLSU5LMks1SGgxUjZfMkdmMUs1OG5uSnd1bENuSVVRSlhSSU83VEd3" + "WFJWOVlfa1hzS0pmREpUVzNnTWJ1UGNGc3p0Vkx3MHpyV04yQXE3YWVLVFI2" + "MFNyeVgzMlBWZkhxNjlzYnUwcnJWLUZMZ2NvMUpBVWlYRlNaY2Q5cVF6WSIs" + "Imlzc3VlZF9hdCI6MTQ0MTUxNTY1OCwidXNlcl9pZCI6IjEwMTAxNDk2NTUz" + "NDg2NjExIn0" + ) + + def test_parse_signed_request_when_erroneous(self): + result = facebook.parse_signed_request( + signed_request="corrupted.payload", app_secret=self.secret + ) + self.assertFalse(result) + + def test_parse_signed_request_when_correct(self): + result = facebook.parse_signed_request( + signed_request=self.cookie, app_secret=self.secret + ) + + self.assertTrue(result) + self.assertTrue("issued_at" in result) + self.assertTrue("code" in result) + self.assertTrue("user_id" in result) + self.assertTrue("algorithm" in result) diff --git a/test/test_versions.py b/test/test_versions.py new file mode 100644 index 00000000..7cb33970 --- /dev/null +++ b/test/test_versions.py @@ -0,0 +1,37 @@ +import facebook +from . import FacebookTestCase + + +class FacebookAPIVersionTestCase(FacebookTestCase): + """Test if using the correct version of Graph API.""" + + def test_no_version(self): + graph = facebook.GraphAPI() + self.assertNotEqual(graph.version, None, "Version should not be None.") + self.assertNotEqual( + graph.version, "", "Version should not be an empty string." + ) + + def test_valid_versions(self): + for version in facebook.VALID_API_VERSIONS: + graph = facebook.GraphAPI(version=version) + self.assertEqual(str(graph.get_version()), version) + + def test_invalid_version(self): + self.assertRaises( + facebook.GraphAPIError, facebook.GraphAPI, version=1.2 + ) + + def test_invalid_format(self): + self.assertRaises( + facebook.GraphAPIError, facebook.GraphAPI, version="2.a" + ) + self.assertRaises( + facebook.GraphAPIError, facebook.GraphAPI, version="a.1" + ) + self.assertRaises( + facebook.GraphAPIError, facebook.GraphAPI, version=2.23 + ) + self.assertRaises( + facebook.GraphAPIError, facebook.GraphAPI, version="2.23" + ) diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/test_facebook.py b/tests/test_facebook.py deleted file mode 100644 index f8ca0ae2..00000000 --- a/tests/test_facebook.py +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env python -# -# Copyright 2013 Martey Dodoo -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -import facebook -import os -import unittest - - -class FacebookTestCase(unittest.TestCase): - """Sets up application ID and secret from environment.""" - def setUp(self): - try: - self.app_id = os.environ["FACEBOOK_APP_ID"] - self.secret = os.environ["FACEBOOK_SECRET"] - except KeyError: - raise Exception("FACEBOOK_APP_ID and FACEBOOK_SECRET " - "must be set as environmental variables.") - - -class TestGetAppAccessToken(FacebookTestCase): - """ - Test if application access token is returned properly. - - Note that this only tests if the returned token is a string, not - whether it is valid. - - """ - def test_get_app_access_token(self): - assert(isinstance(facebook.get_app_access_token( - self.app_id, self.secret), str)) - - -if __name__ == '__main__': - unittest.main()