├── examples ├── newsfeed │ ├── static │ │ ├── robots.txt │ │ ├── favicon.ico │ │ └── base.css │ ├── app.yaml │ ├── templates │ │ ├── index.html │ │ ├── base.html │ │ └── home.html │ └── facebookclient.py ├── appengine │ ├── app.yaml │ ├── example.html │ └── example.py ├── oauth │ ├── app.yaml │ ├── oauth.html │ └── facebookoauth.py └── tornado │ ├── schema.sql │ ├── example.html │ └── example.py ├── setup.py ├── readme.md └── src └── facebook.py /examples/newsfeed/static/robots.txt: -------------------------------------------------------------------------------- 1 | User-Agent: * 2 | Disallow: / 3 | -------------------------------------------------------------------------------- /examples/newsfeed/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buzz/python-sdk/master/examples/newsfeed/static/favicon.ico -------------------------------------------------------------------------------- /examples/appengine/app.yaml: -------------------------------------------------------------------------------- 1 | application: facebook-example 2 | version: 1 3 | runtime: python 4 | api_version: 1 5 | 6 | handlers: 7 | - url: /.* 8 | script: example.py 9 | -------------------------------------------------------------------------------- /examples/oauth/app.yaml: -------------------------------------------------------------------------------- 1 | application: facebook-example 2 | version: 1 3 | runtime: python 4 | api_version: 1 5 | 6 | handlers: 7 | - url: /.* 8 | script: facebookoauth.py 9 | -------------------------------------------------------------------------------- /examples/newsfeed/app.yaml: -------------------------------------------------------------------------------- 1 | application: facebook-example 2 | version: 1 3 | runtime: python 4 | api_version: 1 5 | 6 | handlers: 7 | - url: /static 8 | static_dir: static 9 | 10 | - url: /favicon\.ico 11 | static_files: static/favicon.ico 12 | upload: static/favicon.ico 13 | 14 | - url: /robots\.txt 15 | static_files: static/robots.txt 16 | upload: static/robots.txt 17 | 18 | - url: /.* 19 | script: facebookclient.py 20 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | from distutils.core import setup 4 | 5 | setup( 6 | name='facebook-python-sdk', 7 | version='0.1', 8 | 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.', 9 | author='Facebook', 10 | url='http://github.com/facebook/python-sdk', 11 | package_dir={'': 'src'}, 12 | py_modules=[ 13 | 'facebook', 14 | ], 15 | ) 16 | -------------------------------------------------------------------------------- /examples/newsfeed/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block body %} 4 |

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:

5 | 6 |

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

7 | {% endblock %} 8 | -------------------------------------------------------------------------------- /examples/oauth/oauth.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Facebook OAuth Example 6 | 7 | 8 | {% if current_user %} 9 |

10 |

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

11 |

Log out

12 | {% else %} 13 |

You are not yet logged into this site

14 |

Log in with Facebook

15 | {% endif %} 16 | 17 | 18 | -------------------------------------------------------------------------------- /examples/tornado/schema.sql: -------------------------------------------------------------------------------- 1 | -- Copyright 2010 Facebook 2 | -- 3 | -- Licensed under the Apache License, Version 2.0 (the "License"); you may 4 | -- not use this file except in compliance with the License. You may obtain 5 | -- a copy of the License at 6 | -- 7 | -- http://www.apache.org/licenses/LICENSE-2.0 8 | -- 9 | -- Unless required by applicable law or agreed to in writing, software 10 | -- distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | -- WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | -- License for the specific language governing permissions and limitations 13 | -- under the License. 14 | 15 | -- To create the database: 16 | -- CREATE DATABASE example; 17 | -- GRANT ALL PRIVILEGES ON example.* TO 'example'@'localhost' IDENTIFIED BY 'example'; 18 | -- 19 | -- To reload the tables: 20 | -- mysql --user=example --password=example --database=example < schema.sql 21 | 22 | SET SESSION storage_engine = "InnoDB"; 23 | SET SESSION time_zone = "+0:00"; 24 | ALTER DATABASE CHARACTER SET "utf8"; 25 | 26 | DROP TABLE IF EXISTS users; 27 | CREATE TABLE users ( 28 | id VARCHAR(25) NOT NULL PRIMARY KEY, 29 | name VARCHAR(256) NOT NULL, 30 | profile_url VARCHAR(512) NOT NULL, 31 | access_token VARCHAR(512) NOT NULL, 32 | updated TIMESTAMP NOT NULL 33 | ); 34 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | Facebook Python SDK 2 | ==== 3 | 4 | This client library is designed to support the 5 | [Facebook Graph API](http://developers.facebook.com/docs/api) and the official 6 | [Facebook JavaScript SDK](http://github.com/facebook/connect-js), which is 7 | the canonical way to implement Facebook authentication. You can read more 8 | about the Graph API at [http://developers.facebook.com/docs/api](http://developers.facebook.com/docs/api). 9 | 10 | Basic usage: 11 | 12 | graph = facebook.GraphAPI(oauth_access_token) 13 | profile = graph.get_object("me") 14 | friends = graph.get_connections("me", "friends") 15 | graph.put_object("me", "feed", message="I am writing on my wall!") 16 | 17 | If you are using the module within a web application with the 18 | [JavaScript SDK](http://github.com/facebook/connect-js), you can also use the 19 | module to use Facebook for login, parsing the cookie set by the JavaScript SDK 20 | for logged in users. For example, in Google AppEngine, you could get the 21 | profile of the logged in user with: 22 | 23 | user = facebook.get_user_from_cookie(self.request.cookies, key, secret) 24 | if user: 25 | graph = facebook.GraphAPI(user["oauth_access_token"]) 26 | profile = graph.get_object("me") 27 | friends = graph.get_connections("me", "friends") 28 | 29 | You can see a full AppEngine example application in examples/appengine. 30 | -------------------------------------------------------------------------------- /examples/appengine/example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Facebook Example 6 | 7 | 8 | 9 | 10 | {% if current_user %} 11 |

12 |

Hello, {{ current_user.name|escape }}

13 | {% endif %} 14 | 15 |
16 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /examples/tornado/example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Facebook Example 6 | 7 | 8 | 9 | 10 | {% if current_user %} 11 |

12 |

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

13 | {% end %} 14 | 15 |
16 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /examples/newsfeed/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {% block title %}Facebook Client Example{% endblock %} 6 | 7 | {% block head %}{% endblock %} 8 | 9 | 10 |
This application is a demo of the Facebook Graph API, the core part of the Facebook Platform. See source code »
11 |
{% block body %}{% endblock %}
12 |
13 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /examples/newsfeed/templates/home.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block body %} 4 | 8 |
9 | 10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | 20 | {% for post in news_feed.data %} 21 |
22 |
23 |
24 |
25 | {{ post.from.name|escape }} 26 | {% if post.message %}{{ post.message|escape }}{% endif %} 27 |
28 | {% if post.caption or post.picture %} 29 |
30 | {% if post.picture %} 31 |
32 | {% endif %} 33 | {% if post.name %} 34 | 35 | {% endif %} 36 | {% if post.caption %} 37 |
{{ post.caption|escape }}
38 | {% endif %} 39 | {% if post.description %} 40 |
{{ post.description|escape }}
41 | {% endif %} 42 |
43 | {% endif %} 44 |
45 | {{ post.created_time|timesince }} ago 46 |
47 |
48 |
49 | {% endfor %} 50 | 51 |
52 | {% endblock %} 53 | -------------------------------------------------------------------------------- /examples/newsfeed/static/base.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: white; 3 | margin: 0; 4 | } 5 | 6 | body, 7 | input, 8 | textarea { 9 | color: #333; 10 | font-family: "Lucida Grande", Tahoma, Verdana, Arial, sans-serif; 11 | font-size: 13px; 12 | } 13 | 14 | a { 15 | color: #3b5998; 16 | text-decoration: none; 17 | } 18 | 19 | a:hover { 20 | text-decoration: underline; 21 | } 22 | 23 | img { 24 | border: 0; 25 | } 26 | 27 | table { 28 | border: 0; 29 | border-collapse: collapse; 30 | border-spacing: 0; 31 | } 32 | 33 | td { 34 | border: 0; 35 | padding: 0; 36 | } 37 | 38 | #promo { 39 | background: #eee; 40 | padding: 8px; 41 | border-bottom: 1px solid #ccc; 42 | color: gray; 43 | text-align: center; 44 | } 45 | 46 | #body { 47 | max-width: 800px; 48 | margin: auto; 49 | padding: 20px; 50 | } 51 | 52 | #header h1 { 53 | margin: 0; 54 | padding: 0; 55 | font-size: 15px; 56 | line-height: 25px; 57 | } 58 | 59 | #header .button { 60 | float: right; 61 | } 62 | 63 | #content { 64 | clear: both; 65 | margin-top: 15px; 66 | } 67 | 68 | .clearfix:after { 69 | clear: both; 70 | content: "."; 71 | display: block; 72 | font-size: 0; 73 | height: 0; 74 | line-height: 0; 75 | visibility: hidden; 76 | } 77 | 78 | .clearfix { 79 | display: block; 80 | zoom: 1; 81 | } 82 | 83 | .feed .entry { 84 | padding-top: 9px; 85 | border-top: 1px solid #eee; 86 | margin-top: 9px; 87 | } 88 | 89 | .feed .entry .profile { 90 | float: left; 91 | line-height: 0; 92 | } 93 | 94 | .feed .entry .profile img { 95 | width: 50px; 96 | height: 50px; 97 | } 98 | 99 | .feed .entry .body { 100 | margin-left: 60px; 101 | } 102 | 103 | .feed .entry .name { 104 | font-weight: bold; 105 | } 106 | 107 | .feed .entry .attachment { 108 | font-size: 11px; 109 | line-height: 15px; 110 | margin-top: 8px; 111 | margin-bottom: 8px; 112 | color: gray; 113 | } 114 | 115 | .feed .entry .attachment.nopicture { 116 | border-left: 2px solid #ccc; 117 | padding-left: 10px; 118 | } 119 | 120 | .feed .entry .attachment .picture { 121 | line-height: 0; 122 | float: left; 123 | padding-right: 10px; 124 | } 125 | 126 | .feed .entry .attachment .picture img { 127 | border: 1px solid #ccc; 128 | padding: 3px; 129 | } 130 | 131 | .feed .entry .attachment .picture a:hover img { 132 | border-color: #3b5998; 133 | } 134 | 135 | .feed .entry .info { 136 | font-size: 11px; 137 | line-height: 17px; 138 | margin-top: 3px; 139 | color: gray; 140 | } 141 | 142 | .feed .entry .info.icon { 143 | background-position: left center; 144 | background-repeat: no-repeat; 145 | padding-left: 20px; 146 | } 147 | 148 | .feed .post .textbox { 149 | margin-right: 6px; 150 | } 151 | 152 | .feed .post .textbox textarea { 153 | margin: 0; 154 | border: 1px solid #bbb; 155 | border-top-color: #aeaeae; 156 | padding: 2px; 157 | width: 100%; 158 | } 159 | 160 | .feed .post .buttons { 161 | text-align: right; 162 | } 163 | -------------------------------------------------------------------------------- /examples/appengine/example.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2010 Facebook 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | """A barebones AppEngine application that uses Facebook for login.""" 18 | 19 | FACEBOOK_APP_ID = "your app id" 20 | FACEBOOK_APP_SECRET = "your app secret" 21 | 22 | import facebook 23 | import os.path 24 | import wsgiref.handlers 25 | 26 | from google.appengine.ext import db 27 | from google.appengine.ext import webapp 28 | from google.appengine.ext.webapp import util 29 | from google.appengine.ext.webapp import template 30 | 31 | 32 | class User(db.Model): 33 | id = db.StringProperty(required=True) 34 | created = db.DateTimeProperty(auto_now_add=True) 35 | updated = db.DateTimeProperty(auto_now=True) 36 | name = db.StringProperty(required=True) 37 | profile_url = db.StringProperty(required=True) 38 | access_token = db.StringProperty(required=True) 39 | 40 | 41 | class BaseHandler(webapp.RequestHandler): 42 | """Provides access to the active Facebook user in self.current_user 43 | 44 | The property is lazy-loaded on first access, using the cookie saved 45 | by the Facebook JavaScript SDK to determine the user ID of the active 46 | user. See http://developers.facebook.com/docs/authentication/ for 47 | more information. 48 | """ 49 | @property 50 | def current_user(self): 51 | if not hasattr(self, "_current_user"): 52 | self._current_user = None 53 | cookie = facebook.get_user_from_cookie( 54 | self.request.cookies, FACEBOOK_APP_ID, FACEBOOK_APP_SECRET) 55 | if cookie: 56 | # Store a local instance of the user data so we don't need 57 | # a round-trip to Facebook on every request 58 | user = User.get_by_key_name(cookie["uid"]) 59 | if not user: 60 | graph = facebook.GraphAPI(cookie["access_token"]) 61 | profile = graph.get_object("me") 62 | user = User(key_name=str(profile["id"]), 63 | id=str(profile["id"]), 64 | name=profile["name"], 65 | profile_url=profile["link"], 66 | access_token=cookie["access_token"]) 67 | user.put() 68 | elif user.access_token != cookie["access_token"]: 69 | user.access_token = cookie["access_token"] 70 | user.put() 71 | self._current_user = user 72 | return self._current_user 73 | 74 | 75 | class HomeHandler(BaseHandler): 76 | def get(self): 77 | path = os.path.join(os.path.dirname(__file__), "example.html") 78 | args = dict(current_user=self.current_user, 79 | facebook_app_id=FACEBOOK_APP_ID) 80 | self.response.out.write(template.render(path, args)) 81 | 82 | 83 | def main(): 84 | util.run_wsgi_app(webapp.WSGIApplication([(r"/", HomeHandler)])) 85 | 86 | 87 | if __name__ == "__main__": 88 | main() 89 | -------------------------------------------------------------------------------- /examples/tornado/example.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2010 Facebook 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | """A barebones Tornado application that uses Facebook for login. 18 | 19 | Assumes a database with a schema as specified in schema.sql. We store a 20 | local copy of basic user data so we don't need to make a round-trip to 21 | the Facebook API on every request once a user has logged in. 22 | """ 23 | 24 | import facebook 25 | import tornado.database 26 | import tornado.httpserver 27 | import tornado.options 28 | import tornado.web 29 | 30 | from tornado.options import define, options 31 | 32 | define("port", default=8888, help="run on the given port", type=int) 33 | define("facebook_app_id", help="Facebook Application ID") 34 | define("facebook_app_secret", help="Facebook Application Secret") 35 | define("mysql_host", help="MySQL database host") 36 | define("mysql_database", help="MySQL database database") 37 | define("mysql_user", help="MySQL database user") 38 | define("mysql_password", help="MySQL database password") 39 | 40 | 41 | class BaseHandler(tornado.web.RequestHandler): 42 | """Implements authentication via the Facebook JavaScript SDK cookie.""" 43 | def get_current_user(self): 44 | cookies = dict((n, self.cookies[n].value) for n in self.cookies.keys()) 45 | cookie = facebook.get_user_from_cookie( 46 | cookies, options.facebook_app_id, options.facebook_app_secret) 47 | if not cookie: return None 48 | user = self.db.get( 49 | "SELECT * FROM users WHERE id = %s", cookie["uid"]) 50 | if not user: 51 | # TODO: Make this fetch async rather than blocking 52 | graph = facebook.GraphAPI(cookie["access_token"]) 53 | profile = graph.get_object("me") 54 | self.db.execute( 55 | "REPLACE INTO users (id, name, profile_url, access_token) " 56 | "VALUES (%s,%s,%s,%s)", profile["id"], profile["name"], 57 | profile["link"], cookie["access_token"]) 58 | user = self.db.get( 59 | "SELECT * FROM users WHERE id = %s", profile["id"]) 60 | elif user.access_token != cookie["access_token"]: 61 | self.db.execute( 62 | "UPDATE users SET access_token = %s WHERE id = %s", 63 | cookie["access_token"], user.id) 64 | return user 65 | 66 | @property 67 | def db(self): 68 | if not hasattr(BaseHandler, "_db"): 69 | BaseHandler._db = tornado.database.Connection( 70 | host=options.mysql_host, database=options.mysql_database, 71 | user=options.mysql_user, password=options.mysql_password) 72 | return BaseHandler._db 73 | 74 | 75 | class MainHandler(BaseHandler): 76 | def get(self): 77 | self.render("example.html", options=options) 78 | 79 | 80 | def main(): 81 | tornado.options.parse_command_line() 82 | http_server = tornado.httpserver.HTTPServer(tornado.web.Application([ 83 | (r"/", MainHandler), 84 | ])) 85 | http_server.listen(options.port) 86 | tornado.ioloop.IOLoop.instance().start() 87 | 88 | 89 | if __name__ == "__main__": 90 | main() 91 | -------------------------------------------------------------------------------- /examples/newsfeed/facebookclient.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2010 Facebook 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | """A Facebook stream client written against the Facebook Graph API.""" 18 | 19 | FACEBOOK_APP_ID = "your app id" 20 | FACEBOOK_APP_SECRET = "your app secret" 21 | 22 | import datetime 23 | import facebook 24 | import os 25 | import os.path 26 | import wsgiref.handlers 27 | 28 | from google.appengine.ext import db 29 | from google.appengine.ext import webapp 30 | from google.appengine.ext.webapp import util 31 | from google.appengine.ext.webapp import template 32 | 33 | 34 | class User(db.Model): 35 | id = db.StringProperty(required=True) 36 | created = db.DateTimeProperty(auto_now_add=True) 37 | updated = db.DateTimeProperty(auto_now=True) 38 | name = db.StringProperty(required=True) 39 | profile_url = db.StringProperty(required=True) 40 | access_token = db.StringProperty(required=True) 41 | 42 | 43 | class BaseHandler(webapp.RequestHandler): 44 | """Provides access to the active Facebook user in self.current_user 45 | 46 | The property is lazy-loaded on first access, using the cookie saved 47 | by the Facebook JavaScript SDK to determine the user ID of the active 48 | user. See http://developers.facebook.com/docs/authentication/ for 49 | more information. 50 | """ 51 | @property 52 | def current_user(self): 53 | """Returns the active user, or None if the user has not logged in.""" 54 | if not hasattr(self, "_current_user"): 55 | self._current_user = None 56 | cookie = facebook.get_user_from_cookie( 57 | self.request.cookies, FACEBOOK_APP_ID, FACEBOOK_APP_SECRET) 58 | if cookie: 59 | # Store a local instance of the user data so we don't need 60 | # a round-trip to Facebook on every request 61 | user = User.get_by_key_name(cookie["uid"]) 62 | if not user: 63 | graph = facebook.GraphAPI(cookie["access_token"]) 64 | profile = graph.get_object("me") 65 | user = User(key_name=str(profile["id"]), 66 | id=str(profile["id"]), 67 | name=profile["name"], 68 | profile_url=profile["link"], 69 | access_token=cookie["access_token"]) 70 | user.put() 71 | elif user.access_token != cookie["access_token"]: 72 | user.access_token = cookie["access_token"] 73 | user.put() 74 | self._current_user = user 75 | return self._current_user 76 | 77 | @property 78 | def graph(self): 79 | """Returns a Graph API client for the current user.""" 80 | if not hasattr(self, "_graph"): 81 | if self.current_user: 82 | self._graph = facebook.GraphAPI(self.current_user.access_token) 83 | else: 84 | self._graph = facebook.GraphAPI() 85 | return self._graph 86 | 87 | def render(self, path, **kwargs): 88 | args = dict(current_user=self.current_user, 89 | facebook_app_id=FACEBOOK_APP_ID) 90 | args.update(kwargs) 91 | path = os.path.join(os.path.dirname(__file__), "templates", path) 92 | self.response.out.write(template.render(path, args)) 93 | 94 | 95 | class HomeHandler(BaseHandler): 96 | def get(self): 97 | if not self.current_user: 98 | self.render("index.html") 99 | return 100 | try: 101 | news_feed = self.graph.get_connections("me", "home") 102 | except facebook.GraphAPIError: 103 | self.render("index.html") 104 | return 105 | except: 106 | news_feed = {"data": []} 107 | for post in news_feed["data"]: 108 | post["created_time"] = datetime.datetime.strptime( 109 | post["created_time"], "%Y-%m-%dT%H:%M:%S+0000") + \ 110 | datetime.timedelta(hours=7) 111 | self.render("home.html", news_feed=news_feed) 112 | 113 | 114 | class PostHandler(BaseHandler): 115 | def post(self): 116 | message = self.request.get("message") 117 | if not self.current_user or not message: 118 | self.redirect("/") 119 | return 120 | try: 121 | self.graph.put_wall_post(message) 122 | except: 123 | pass 124 | self.redirect("/") 125 | 126 | 127 | def main(): 128 | debug = os.environ.get("SERVER_SOFTWARE", "").startswith("Development/") 129 | util.run_wsgi_app(webapp.WSGIApplication([ 130 | (r"/", HomeHandler), 131 | (r"/post", PostHandler), 132 | ], debug=debug)) 133 | 134 | 135 | if __name__ == "__main__": 136 | main() 137 | -------------------------------------------------------------------------------- /examples/oauth/facebookoauth.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2010 Facebook 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | """A barebones AppEngine application that uses Facebook for login. 18 | 19 | This application uses OAuth 2.0 directly rather than relying on Facebook's 20 | JavaScript SDK for login. It also accesses the Facebook Graph API directly 21 | rather than using the Python SDK. It is designed to illustrate how easy 22 | it is to use the Facebook Platform without any third party code. 23 | 24 | See the "appengine" directory for an example using the JavaScript SDK. 25 | Using JavaScript is recommended if it is feasible for your application, 26 | as it handles some complex authentication states that can only be detected 27 | in client-side code. 28 | """ 29 | 30 | FACEBOOK_APP_ID = "your app id" 31 | FACEBOOK_APP_SECRET = "your app secret" 32 | 33 | import base64 34 | import cgi 35 | import Cookie 36 | import email.utils 37 | import hashlib 38 | import hmac 39 | import logging 40 | import os.path 41 | import time 42 | import urllib 43 | import wsgiref.handlers 44 | 45 | from django.utils import simplejson as json 46 | from google.appengine.ext import db 47 | from google.appengine.ext import webapp 48 | from google.appengine.ext.webapp import util 49 | from google.appengine.ext.webapp import template 50 | 51 | 52 | class User(db.Model): 53 | id = db.StringProperty(required=True) 54 | created = db.DateTimeProperty(auto_now_add=True) 55 | updated = db.DateTimeProperty(auto_now=True) 56 | name = db.StringProperty(required=True) 57 | profile_url = db.StringProperty(required=True) 58 | access_token = db.StringProperty(required=True) 59 | 60 | 61 | class BaseHandler(webapp.RequestHandler): 62 | @property 63 | def current_user(self): 64 | """Returns the logged in Facebook user, or None if unconnected.""" 65 | if not hasattr(self, "_current_user"): 66 | self._current_user = None 67 | user_id = parse_cookie(self.request.cookies.get("fb_user")) 68 | if user_id: 69 | self._current_user = User.get_by_key_name(user_id) 70 | return self._current_user 71 | 72 | 73 | class HomeHandler(BaseHandler): 74 | def get(self): 75 | path = os.path.join(os.path.dirname(__file__), "oauth.html") 76 | args = dict(current_user=self.current_user) 77 | self.response.out.write(template.render(path, args)) 78 | 79 | 80 | class LoginHandler(BaseHandler): 81 | def get(self): 82 | verification_code = self.request.get("code") 83 | args = dict(client_id=FACEBOOK_APP_ID, redirect_uri=self.request.path_url) 84 | if self.request.get("code"): 85 | args["client_secret"] = FACEBOOK_APP_SECRET 86 | args["code"] = self.request.get("code") 87 | response = cgi.parse_qs(urllib.urlopen( 88 | "https://graph.facebook.com/oauth/access_token?" + 89 | urllib.urlencode(args)).read()) 90 | access_token = response["access_token"][-1] 91 | 92 | # Download the user profile and cache a local instance of the 93 | # basic profile info 94 | profile = json.load(urllib.urlopen( 95 | "https://graph.facebook.com/me?" + 96 | urllib.urlencode(dict(access_token=access_token)))) 97 | user = User(key_name=str(profile["id"]), id=str(profile["id"]), 98 | name=profile["name"], access_token=access_token, 99 | profile_url=profile["link"]) 100 | user.put() 101 | set_cookie(self.response, "fb_user", str(profile["id"]), 102 | expires=time.time() + 30 * 86400) 103 | self.redirect("/") 104 | else: 105 | self.redirect( 106 | "https://graph.facebook.com/oauth/authorize?" + 107 | urllib.urlencode(args)) 108 | 109 | 110 | class LogoutHandler(BaseHandler): 111 | def get(self): 112 | set_cookie(self.response, "fb_user", "", expires=time.time() - 86400) 113 | self.redirect("/") 114 | 115 | 116 | def set_cookie(response, name, value, domain=None, path="/", expires=None): 117 | """Generates and signs a cookie for the give name/value""" 118 | timestamp = str(int(time.time())) 119 | value = base64.b64encode(value) 120 | signature = cookie_signature(value, timestamp) 121 | cookie = Cookie.BaseCookie() 122 | cookie[name] = "|".join([value, timestamp, signature]) 123 | cookie[name]["path"] = path 124 | if domain: cookie[name]["domain"] = domain 125 | if expires: 126 | cookie[name]["expires"] = email.utils.formatdate( 127 | expires, localtime=False, usegmt=True) 128 | response.headers._headers.append(("Set-Cookie", cookie.output()[12:])) 129 | 130 | 131 | def parse_cookie(value): 132 | """Parses and verifies a cookie value from set_cookie""" 133 | if not value: return None 134 | parts = value.split("|") 135 | if len(parts) != 3: return None 136 | if cookie_signature(parts[0], parts[1]) != parts[2]: 137 | logging.warning("Invalid cookie signature %r", value) 138 | return None 139 | timestamp = int(parts[1]) 140 | if timestamp < time.time() - 30 * 86400: 141 | logging.warning("Expired cookie %r", value) 142 | return None 143 | try: 144 | return base64.b64decode(parts[0]).strip() 145 | except: 146 | return None 147 | 148 | 149 | def cookie_signature(*parts): 150 | """Generates a cookie signature. 151 | 152 | We use the Facebook app secret since it is different for every app (so 153 | people using this example don't accidentally all use the same secret). 154 | """ 155 | hash = hmac.new(FACEBOOK_APP_SECRET, digestmod=hashlib.sha1) 156 | for part in parts: hash.update(part) 157 | return hash.hexdigest() 158 | 159 | 160 | def main(): 161 | util.run_wsgi_app(webapp.WSGIApplication([ 162 | (r"/", HomeHandler), 163 | (r"/auth/login", LoginHandler), 164 | (r"/auth/logout", LogoutHandler), 165 | ])) 166 | 167 | 168 | if __name__ == "__main__": 169 | main() 170 | -------------------------------------------------------------------------------- /src/facebook.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2010 Facebook 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | """Python client library for the Facebook Platform. 18 | 19 | This client library is designed to support the Graph API and the official 20 | Facebook JavaScript SDK, which is the canonical way to implement 21 | Facebook authentication. Read more about the Graph API at 22 | http://developers.facebook.com/docs/api. You can download the Facebook 23 | JavaScript SDK at http://github.com/facebook/connect-js/. 24 | 25 | If your application is using Google AppEngine's webapp framework, your 26 | usage of this module might look like this: 27 | 28 | user = facebook.get_user_from_cookie(self.request.cookies, key, secret) 29 | if user: 30 | graph = facebook.GraphAPI(user["access_token"]) 31 | profile = graph.get_object("me") 32 | friends = graph.get_connections("me", "friends") 33 | 34 | """ 35 | 36 | import cgi 37 | import hashlib 38 | import time 39 | import urllib 40 | 41 | # Find a JSON parser 42 | try: 43 | import json 44 | _parse_json = lambda s: json.loads(s) 45 | except ImportError: 46 | try: 47 | import simplejson 48 | _parse_json = lambda s: simplejson.loads(s) 49 | except ImportError: 50 | # For Google AppEngine 51 | from django.utils import simplejson 52 | _parse_json = lambda s: simplejson.loads(s) 53 | 54 | 55 | class GraphAPI(object): 56 | """A client for the Facebook Graph API. 57 | 58 | See http://developers.facebook.com/docs/api for complete documentation 59 | for the API. 60 | 61 | The Graph API is made up of the objects in Facebook (e.g., people, pages, 62 | events, photos) and the connections between them (e.g., friends, 63 | photo tags, and event RSVPs). This client provides access to those 64 | primitive types in a generic way. For example, given an OAuth access 65 | token, this will fetch the profile of the active user and the list 66 | of the user's friends: 67 | 68 | graph = facebook.GraphAPI(access_token) 69 | user = graph.get_object("me") 70 | friends = graph.get_connections(user["id"], "friends") 71 | 72 | You can see a list of all of the objects and connections supported 73 | by the API at http://developers.facebook.com/docs/reference/api/. 74 | 75 | You can obtain an access token via OAuth or by using the Facebook 76 | JavaScript SDK. See http://developers.facebook.com/docs/authentication/ 77 | for details. 78 | 79 | If you are using the JavaScript SDK, you can use the 80 | get_user_from_cookie() method below to get the OAuth access token 81 | for the active user from the cookie saved by the SDK. 82 | 83 | A locale parameter can be passed that overrides the default 84 | locale. 85 | """ 86 | def __init__(self, access_token=None, locale=None): 87 | self.access_token = access_token 88 | self.locale = locale 89 | 90 | def get_object(self, id, **args): 91 | """Fetchs the given object from the graph.""" 92 | return self.request(id, args) 93 | 94 | def get_objects(self, ids, **args): 95 | """Fetchs all of the given object from the graph. 96 | 97 | We return a map from ID to object. If any of the IDs are invalid, 98 | we raise an exception. 99 | """ 100 | args["ids"] = ",".join(ids) 101 | return self.request("", args) 102 | 103 | def get_connections(self, id, connection_name, **args): 104 | """Fetchs the connections for given object.""" 105 | return self.request(id + "/" + connection_name, args) 106 | 107 | def put_object(self, parent_object, connection_name, **data): 108 | """Writes the given object to the graph, connected to the given parent. 109 | 110 | For example, 111 | 112 | graph.put_object("me", "feed", message="Hello, world") 113 | 114 | writes "Hello, world" to the active user's wall. Likewise, this 115 | will comment on a the first post of the active user's feed: 116 | 117 | feed = graph.get_connections("me", "feed") 118 | post = feed["data"][0] 119 | graph.put_object(post["id"], "comments", message="First!") 120 | 121 | See http://developers.facebook.com/docs/api#publishing for all of 122 | the supported writeable objects. 123 | 124 | Most write operations require extended permissions. For example, 125 | publishing wall posts requires the "publish_stream" permission. See 126 | http://developers.facebook.com/docs/authentication/ for details about 127 | extended permissions. 128 | """ 129 | assert self.access_token, "Write operations require an access token" 130 | return self.request(parent_object + "/" + connection_name, post_args=data) 131 | 132 | def put_wall_post(self, message, attachment={}, profile_id="me"): 133 | """Writes a wall post to the given profile's wall. 134 | 135 | We default to writing to the authenticated user's wall if no 136 | profile_id is specified. 137 | 138 | attachment adds a structured attachment to the status message being 139 | posted to the Wall. It should be a dictionary of the form: 140 | 141 | {"name": "Link name" 142 | "link": "http://www.example.com/", 143 | "caption": "{*actor*} posted a new review", 144 | "description": "This is a longer description of the attachment", 145 | "picture": "http://www.example.com/thumbnail.jpg"} 146 | 147 | """ 148 | return self.put_object(profile_id, "feed", message=message, **attachment) 149 | 150 | def put_comment(self, object_id, message): 151 | """Writes the given comment on the given post.""" 152 | return self.put_object(object_id, "comments", message=message) 153 | 154 | def put_like(self, object_id): 155 | """Likes the given post.""" 156 | return self.put_object(object_id, "likes") 157 | 158 | def delete_object(self, id): 159 | """Deletes the object with the given ID from the graph.""" 160 | self.request(id, post_args={"method": "delete"}) 161 | 162 | def request(self, path, args=None, post_args=None): 163 | """Fetches the given path in the Graph API. 164 | 165 | We translate args to a valid query string. If post_args is given, 166 | we send a POST request to the given path with the given arguments. 167 | """ 168 | if not args: args = {} 169 | if self.access_token: 170 | if post_args is not None: 171 | post_args["access_token"] = self.access_token 172 | else: 173 | args["access_token"] = self.access_token 174 | if self.locale is not None: 175 | args["locale"] = self.locale 176 | post_data = None if post_args is None else urllib.urlencode(post_args) 177 | file = urllib.urlopen("https://graph.facebook.com/" + path + "?" + 178 | urllib.urlencode(args), post_data) 179 | try: 180 | response = _parse_json(file.read()) 181 | finally: 182 | file.close() 183 | if response.get("error"): 184 | raise GraphAPIError(response["error"]["type"], 185 | response["error"]["message"]) 186 | return response 187 | 188 | 189 | class GraphAPIError(Exception): 190 | def __init__(self, type, message): 191 | Exception.__init__(self, message) 192 | self.type = type 193 | 194 | 195 | def get_user_from_cookie(cookies, app_id, app_secret): 196 | """Parses the cookie set by the official Facebook JavaScript SDK. 197 | 198 | cookies should be a dictionary-like object mapping cookie names to 199 | cookie values. 200 | 201 | If the user is logged in via Facebook, we return a dictionary with the 202 | keys "uid" and "access_token". The former is the user's Facebook ID, 203 | and the latter can be used to make authenticated requests to the Graph API. 204 | If the user is not logged in, we return None. 205 | 206 | Download the official Facebook JavaScript SDK at 207 | http://github.com/facebook/connect-js/. Read more about Facebook 208 | authentication at http://developers.facebook.com/docs/authentication/. 209 | """ 210 | cookie = cookies.get("fbs_" + app_id, "") 211 | if not cookie: return None 212 | args = dict((k, v[-1]) for k, v in cgi.parse_qs(cookie.strip('"')).items()) 213 | payload = "".join(k + "=" + args[k] for k in sorted(args.keys()) 214 | if k != "sig") 215 | sig = hashlib.md5(payload + app_secret).hexdigest() 216 | expires = int(args["expires"]) 217 | if sig == args.get("sig") and (expires == 0 or time.time() < expires): 218 | return args 219 | else: 220 | return None 221 | --------------------------------------------------------------------------------