facebook-utils 0.60.0
Simple utilites for Facebook integration with your website.
License: BSD
Keywords: facebook

facebook_utils
==============

This library was created when a lot of things were possible - or worth doing -
with the Facebook Graph API, and their official Python SDK did not offer some
features and was no longer maintained.

Facebook's Graph API quickly lost many of it's worthwhile features.
Eventually, they pushed for a new Business API and SDK that focuses on other
things. Neither are particularly useful to accomplish the previously
capable tasks. The utility of the Facebook service itself is increasingly
questionable as well.

This library has been lightly updated to support legacy projects that once
used the Graph API and Facebook Login. This library is not intended for robust
usage or long term use.

The current project goals are to have proper typing and modern Python support
for the remaining API utilities as projects move off this package and/or
Facebook's API. New features are not intended, only increased code quality
and integration.

Several tests are broken, as the functionality appears to longer work.
Specifically, the unauthenticated tests for "test_graph__get_object_single" and
"test_graph__get_object_multiple" no longer work, as they attempt to get the
Facebook graph IDs for URLs - which is no longer supported via the Graph API.

Additionally, all tests will fail once API Rate Limits are exceeded.

-----

A collection of utilities for integrating User Accounts with Facebook.com.

Right now this library is focused on oAuth Login and Graph API operations

This library has been a general-purpose Python client, operating in production
for many years.  The functionality may be limited, but it works well and ships
with a detailed test suite.

Purpose
=======

1. Once upon a time, Facebook offered a Python SDK. They dropped it. Boo.

2. When this was first released, there were no other Python SDKs actively developed.

3. There are a handful of Pyramid/Misc Framework utilities that provide a complete
drop-in integration with Facebook.com for account logins and integrations;
This is NOT one of them. Sometimes you need to control the User Experience and
have all your UX customized; if so, this is for you.


Usage
=====

All work is done via the `facebook_utils.core.FacebookHub()` object.

Configure a hub with something like the following:

	from facebook_utils import FacebookHub
    hub = FacebookHub(app_id = x,
                      app_secret = y,
                      enable_app_secretproof = True
                      )

Or make it unuthenticated. It's up to you.

This was originally built/intended for use under the Pyramid environment.

Calling `FacebookPyramid()` will create a new object that subclasses
`FacebookHub()` objects, using  default settings from your .ini and
pulling variables from `request` as needed.

As of v0.5.0 it supports the `appsecret_proof` lockdown on the client level.

Any requests to the hub will attempt to create the `appsecret_proof` hmac if it
is not explicitly provided. It will be based on the `access_token` appearing as
(in the order of precedence):

* the `access_token` kwarg to the `.api_proxy() method
* an `access_token` in the querystring of a retrieved url
* an `access_token` in the POST payload

This will allow you to follow paginated links from the API as-is, upgrading as
needed.


IMPORTANT NOTES
===============

Facebook's API Support is inconsistent with the terminology:

* The API endpoints expect `client_id` and `client_secret`
* The Developer Tools provide `app id` and `app secret`

For the sake of clarity, this library uses the terms `app_id` and `app_secret`
because they are what Facebook's developer dashboard provides. They are translated
into the API Endpoint terms as required.

By default the API calls will be unversioned. You should specify the API version.


Supports Two oAuth Flows
========================

Flow 1 - Server Side
--------------------
1. configure an object with `oauth_code_redirect_uri`
2. consumers click a button on your site, which redirects to
`oauth_code_redirect_uri` -- as provided by `oauth_code__url_dialog()`
3. upon success, users are redirected from Facebook to
`oauth_code_redirect_uri` along with a query param titled `code`
4. you may then call `.oauth_code__get_access_token()` to get an access
token or call `oauth_code__get_access_token_and_profile()` to get the token
and profile data.
5. profile data can be updated with `.graph__get_profile(access_token)`


Flow 2 - Client Side
--------------------
1. configure an object with `oauth_token_redirect_uri`
2. consumers click a button on your site, which redirects to
`oauth_token_redirect_uri` -- as provided by `oauth_token__url_dialog()`
3. upon success, users are redirected from Facebook to
`oauth_token__url_dialog` along with a query param titled `token` and a
hash value titled `#access_token`. The `access_token` is not visible to
the server, and must be transferred to your server via JavaScript or
not-at-all should you simply want to do all your integration in JavaScript.
4. profile data can be obtained with `.graph__get_profile(access_token)`
if you store the access token


Unauthenticated Queries
=======================

Queries without authentication required can be performed using the `.api_proxy`
method.

If you don't have any authentication data, you can create an unauthenticated
hub, which allows you to leverage this library for streamlined requests and
response processing.

    hub = FacebookHub(unauthenticated_hub=True)
    get_data = {'ids': "http://example.com", }
    fb_data = hub.api_proxy(url="""https://graph.facebook.com""",
        expected_format='json.load', get_data=get_data)
    pprint.pprint(fb_data)


Rate Limiting?
==============

Facebook may ratelimit requests.

* see https://developers.facebook.com/docs/graph-api/advanced/rate-limiting

The last response is stored `FacebookHub._last_response` for inspection.

A convenience method will check for the `X-Page-Usage` ratelimiting header:

    print hub.last_response_ratelimited()

If no ratelimiting is set, it will return None.

If Facebook has set ratelimiting, it will convert the JSON-formatted string in
the header into a python dict:

    print hub.last_response_ratelimited()
    > {"call_count"    : x,
       "total_time"    : y,
       "total_cputime" : z
       }


Some Notes
==========

Most methods will let you override the `scope` and `request_uri`.
This shouldn't really be necessary and will probably be deprecated.

Some methods support multiple ways of parsing results.
Until recently, Facebook's API returned values either as url-encoded strings or
as JSON. Now most results are in JSON.


Pyramid Examples
================
define some variables in your .ini files:

file: development.ini

    # the default prefix is fbutils
    fbutils.id = 123
    fbutils.secret = 123
    facebook.app.enable_secretproof = 1
    fbutils.scope = email, user_birthday, user_checkins, offline_access
    fbutils.oauth_code_redirect_uri = http://127.0.0.1:5010/facebook-oauth-redirect
    fbutils.api_version = v2.8
    fbutils.oauth_code_redirect_uri=  http://127.0.0.1:5010/account/facebook-authenticate-oauth?response_type=code
    fbutils.oauth_token_redirect_uri= http://127.0.0.1:5010/account/facebook-authenticate-oauth-token?response_type=token

or:

    # customize the prefix!
    fbutils.prefix = facebook.app
    facebook.app.id = 123
    facebook.app.secret = 123
    facebook.app.enable_secretproof = True
    facebook.app.scope = email, user_birthday, user_checkins, offline_access
    facebook.app.oauth_code_redirect_uri = http://127.0.0.1:5010/facebook-oauth-redirect
    facebook.app.api_version = v2.8
    facebook.app.oauth_code_redirect_uri=  http://127.0.0.1:5010/account/facebook-authenticate-oauth?response_type=code
    facebook.app.oauth_token_redirect_uri= http://127.0.0.1:5010/account/facebook-authenticate-oauth-token?response_type=token


Make sure your endpoints are allowlisted on the Facebook console


Integrate into your views:

    from facebook_utils import FacebookPyramid

    class WebAccount(base.Handler):
        def __new_fb_object(self):
            "Create a new Facebook Object
             note that we can override settings in the .ini files if we want
            "
            fb = FacebookPyramid(self.request,
                                 oauth_code_redirect_uri = self.request.registry.settings['facebook.app.oauth_code_redirect_uri'],
                                 oauth_token_redirect_uri = self.request.registry.settings['facebook.app.oauth_token_redirect_uri'],
                                 )
            return fb

        def sign_up(self):
            "sign up page, which contains a "signup with facebook link"
            fb = self.__new_fb_object()
            return {"project":"MyApp", 'facebook_pyramid':facebook}

        @action(renderer="web/account/facebook_authenticate_oauth.html")
        def facebook_authenticate_oauth(self):
            fb = self.__new_fb_object()
            (access_token,
             profile
             )= fb.oauth_code__get_access_token_and_profile(self.request)
            if profile:
                # congrats, they logged in
                # register the user to your db
                raise HTTPFound(location='/account/home')
            else:
                # boo, that didn't work
                raise HTTPFound(location='/account/sign-up?error=facebook-oauth-failure')
            return {"project":"MyApp"}


Integrate into your template:

    <a class="fancy_button-1" id="signup-start_btn-facebook" href="${facebook_pyramid.oauth_code__url_dialog()}">
        Connect with <strong>Facebook</strong>
    </a>

Graph Operations
================

Every `hub` object has an `api_proxy` method, which can be used to
centralize communication to the Facebook API

Facebook's API isn't very 'standardized' across the board. Some endpoints
return json data, others return urlquoted data. `api_proxy` doesn't care.
it returns a dict from every endpoint, and does the conversion for you.

The `api_proxy` defaults to a json load. certain api calls will pass in
a different `expected_format` argument. The proxy will also handle "batch"
style graph requests.

When the api_proxy encounters an error, it returns `ApiError` or a more
contextual subclass of the that exception class.

The current exception class inheritance is:

    ApiError
        ApiAuthError
            ApiAuthExpiredError
        ApiApplicationError
        ApiResponseError
        ApiRuntimeError
            ApiRuntimeVerirficationFormatError
            ApiRuntimeGrantError
            ApiRuntimeScopeError
            ApiRuntimeGraphMethodError
        ApiUnhandledError
    AuthenticatedHubRequired

`ApiError` instances contain:
    code (Facebook specific, not http code)
    type (as dictacted by Facebook)
    message (possibly dictated by Facebook)
    raised (the trapped error that raised this, if available)
    response (the repsonse in error, if available)

`AuthenticatedHubRequired` will be raised if a non-authenticated hub tries to
perform authenticated actions

The `api_proxy` will catch *most* errors. Since the Facebook API is in constant
flux, uncaught exceptions are raised. There will be a future "ApiUnhandledError".


Testing
=======


Trial Run
---------

1. Set up an App on https://developers.facebook.com

2. You need this information exported in your environment

  * `FBUTILS_APP_ID` : The numeric ID for the app
  * `FBUTILS_APP_SECRET` : You generate this on the Facebook Settings
  * `FBUTILS_ENABLE_SECRETPROOF` : Enclose a HMAC secret signature? 1 or 0
  * `FBUTILS_APP_DOMAIN` : the App's domain. example: dev.example.com
  * `FBUTILS_APP_SCOPE` : The scope. can just be "email"
  * `FBUTILS_REDIRECT_URI_OAUTH_CODE` : the uri to redirect visitors to. this MUST be whitelisted on Facebook's OAuth/Login settings

3. Run

	python test-interactive.py
	
You will see a message like:

    Visit the following url to approve.

    You will be redirected back to the `FBUTILS_REDIRECT_URI_OAUTH_CODE` URI

    > https://www.facebook.com/dialog/oauth?client_id={XXXXX}&scope={XXXXX}&redirect_uri={XXXXX}

Copy/paste that url into a browser window

You will be redirected to a url like:

	> {{https://dev.cliqued.in/account/login/facebook-oauth?response_type=code}}&code={{CODE}}#_=_

Copy the entire `CODE` from the URL and paste it in.  It is okay to leave the
trailing `#_=_` fragment

This will step you through multiple API calls, asking you to visit the
authorization URL each time.

You should notice the authorization code changes each time, however the Facebook
API token which is returned will remain the same.


Unit Tests
----------

Unit Tests require the following environment vars to be set:

    FBUTILS_APP_ID
    FBUTILS_APP_SECRET
    FBUTILS_APP_SCOPE
    FBUTILS_ACCESS_TOKEN
    FBUTILS_APP_DOMAIN
    FBUTILS_ENABLE_SECRETPROOF

it should be simple...

    export FBUTILS_APP_ID="app_id_from_facebook.com"
    export FBUTILS_APP_SECRET="app_secret_from_facebook.com"
    export FBUTILS_APP_SCOPE="email,user_activities,user_status,user_posts"
    export FBUTILS_APP_DOMAIN='allowlisted domain'
    export FBUTILS_ACCESS_TOKEN="from_API_operations, or generate via developer interface"
    export FBUTILS_ENABLE_SECRETPROOF=set if you locked this down on facebook
    export FBUTILS_REDIRECT_URI_OAUTH_CODE= configured on the facebook dashboard


To generate a `FBUTILS_ACCESS_TOKEN` value, you can use the `tests/generate_credentials.py`
script.

Github Tests:
-------------

Managed by Github Secrets
_________________________

* `FBUTILS_APP_SECRET`
* `FBUTILS_ACCESS_TOKEN`

Set in `.github/workflows/python-package.yml`
_____________________________________________

* `FBUTILS_APP_ID`
* `FBUTILS_APP_SCOPE`
* `FBUTILS_APP_DOMAIN`
* `FBUTILS_ENABLE_SECRETPROOF`
* `FBUTILS_REDIRECT_URI_OAUTH_CODE`
* `FBUTILS_REDIRECT_URI_OAUTH_TOKEN`


Integrated Tests
----------------

There is also a `test_interactive.py` file that uses the same environment vars

    python test_interactive.py

That will allow you to step through a few scenarios and set up an integration
with Facebook itself.


:copyright: 2012-2021 by Jonathan Vanasco
license: BSD