Browse files

Finished implementation of the graph api, including demos

  • Loading branch information...
1 parent 86b1200 commit 25bba8dc92677244b9d05c8061706c5817a2a29d @sandsower sandsower committed Nov 7, 2012
View
178 cyclone/auth.py
@@ -49,6 +49,7 @@ def _on_auth(self, user):
from cyclone import escape
from cyclone import httpclient
from cyclone.util import b
+from cyclone.httputil import url_concat
from twisted.python import log
import binascii
@@ -205,6 +206,44 @@ def get_ax_arg(uri):
callback(user)
+
+class OAuth2Mixin(object):
+ """Abstract implementation of OAuth v 2."""
+
+ def authorize_redirect(self, redirect_uri=None, client_id=None,
+ client_secret=None, extra_params=None):
+ """Redirects the user to obtain OAuth authorization for this service.
+
+ Some providers require that you register a Callback
+ URL with your application. You should call this method to log the
+ user in, and then call get_authenticated_user() in the handler
+ you registered as your Callback URL to complete the authorization
+ process.
+ """
+ args = {
+ "redirect_uri": redirect_uri,
+ "client_id": client_id
+ }
+ if extra_params:
+ args.update(extra_params)
+ self.redirect(
+ url_concat(self._OAUTH_AUTHORIZE_URL, args))
+
+ def _oauth_request_token_url(self, redirect_uri=None, client_id=None,
+ client_secret=None, code=None,
+ extra_params=None):
+ url = self._OAUTH_ACCESS_TOKEN_URL
+ args = dict(
+ redirect_uri=redirect_uri,
+ code=code,
+ client_id=client_id,
+ client_secret=client_secret,
+ )
+ if extra_params:
+ args.update(extra_params)
+ return url_concat(url, args)
+
+
class OAuthMixin(object):
"""Abstract implementation of OAuth.
@@ -859,6 +898,145 @@ def _signature(self, args):
return hashlib.md5(body).hexdigest()
+class FacebookGraphMixin(OAuth2Mixin):
+ """Facebook authentication using the new Graph API and OAuth2."""
+ _OAUTH_ACCESS_TOKEN_URL = "https://graph.facebook.com/oauth/access_token?"
+ _OAUTH_AUTHORIZE_URL = "https://graph.facebook.com/oauth/authorize?"
+ _OAUTH_NO_CALLBACKS = False
+
+ def get_authenticated_user(self, redirect_uri, client_id, client_secret,
+ code, callback, extra_fields=None):
+ """Handles the login for the Facebook user, returning a user object.
+
+ Example usage::
+
+ class FacebookGraphLoginHandler(LoginHandler, tornado.auth.FacebookGraphMixin):
+ @tornado.web.asynchronous
+ def get(self):
+ if self.get_argument("code", False):
+ self.get_authenticated_user(
+ redirect_uri='/auth/facebookgraph/',
+ client_id=self.settings["facebook_api_key"],
+ client_secret=self.settings["facebook_secret"],
+ code=self.get_argument("code"),
+ callback=self.async_callback(
+ self._on_login))
+ return
+ self.authorize_redirect(redirect_uri='/auth/facebookgraph/',
+ client_id=self.settings["facebook_api_key"],
+ extra_params={"scope": "read_stream,offline_access"})
+
+ def _on_login(self, user):
+ logging.error(user)
+ self.finish()
+
+ """
+ args = {
+ "redirect_uri": redirect_uri,
+ "code": code,
+ "client_id": client_id,
+ "client_secret": client_secret,
+ }
+
+ fields = set(['id', 'name', 'first_name', 'last_name',
+ 'locale', 'picture', 'link'])
+ if extra_fields:
+ fields.update(extra_fields)
+
+ httpclient.fetch(self._oauth_request_token_url(**args)).addCallback(self.async_callback(self._on_access_token, redirect_uri, client_id,
+ client_secret, callback, fields))
+
+ def _on_access_token(self, redirect_uri, client_id, client_secret,
+ callback, fields, response):
+ if response.error:
+ log.warning('Facebook auth error: %s' % str(response))
+ callback(None)
+ return
+
+ args = escape.parse_qs_bytes(escape.native_str(response.body))
+ session = {
+ "access_token": args["access_token"][-1],
+ "expires": args.get("expires")
+ }
+
+ self.facebook_request(
+ path="/me",
+ callback=self.async_callback(
+ self._on_get_user_info, callback, session, fields),
+ access_token=session["access_token"],
+ fields=",".join(fields)
+ )
+
+ def _on_get_user_info(self, callback, session, fields, user):
+ if user is None:
+ callback(None)
+ return
+
+ fieldmap = {}
+ for field in fields:
+ fieldmap[field] = user.get(field)
+
+ fieldmap.update({"access_token": session["access_token"], "session_expires": session.get("expires")})
+ callback(fieldmap)
+
+ def facebook_request(self, path, callback, access_token=None,
+ post_args=None, **args):
+ """Fetches the given relative API path, e.g., "/btaylor/picture"
+
+ If the request is a POST, post_args should be provided. Query
+ string arguments should be given as keyword arguments.
+
+ An introduction to the Facebook Graph API can be found at
+ http://developers.facebook.com/docs/api
+
+ Many methods require an OAuth access token which you can obtain
+ through authorize_redirect() and get_authenticated_user(). The
+ user returned through that process includes an 'access_token'
+ attribute that can be used to make authenticated requests via
+ this method. Example usage::
+
+ class MainHandler(tornado.web.RequestHandler,
+ tornado.auth.FacebookGraphMixin):
+ @tornado.web.authenticated
+ @tornado.web.asynchronous
+ def get(self):
+ self.facebook_request(
+ "/me/feed",
+ post_args={"message": "I am posting from my Tornado application!"},
+ access_token=self.current_user["access_token"],
+ callback=self.async_callback(self._on_post))
+
+ def _on_post(self, new_entry):
+ if not new_entry:
+ # Call failed; perhaps missing permission?
+ self.authorize_redirect()
+ return
+ self.finish("Posted a message!")
+
+ """
+ url = "https://graph.facebook.com" + path
+ all_args = {}
+ if access_token:
+ all_args["access_token"] = access_token
+ all_args.update(args)
+
+ if all_args:
+ url += "?" + urllib.urlencode(all_args)
+ callback = self.async_callback(self._on_facebook_request, callback)
+ if post_args is not None:
+ httpclient.fetch(url, method="POST", body=urllib.urlencode(post_args)).addCallback(callback)
+ else:
+ httpclient.fetch(url).addCallback(callback)
+
+ def _on_facebook_request(self, callback, response):
+ if response.error:
+ log.warning("Error response %s fetching %s", response.error,
+ response.request.url)
+ callback(None)
+ return
+ callback(escape.json_decode(response.body))
+
+
def _oauth_signature(consumer_token, method, url, parameters={}, token=None):
"""Calculates the HMAC-SHA1 OAuth signature for the given request.
View
8 demos/fbgraphapi/README
@@ -0,0 +1,8 @@
+Running the Tornado Facebook example
+=====================================
+To work with the provided Facebook api key, this example must be
+accessed at http://localhost:8888/ to match the Connect URL set in the
+example application.
+
+To use any other domain, a new Facebook application must be registered
+with a Connect URL set to that domain.
View
115 demos/fbgraphapi/facebook.py
@@ -0,0 +1,115 @@
+#!/usr/bin/env python
+# coding: utf-8
+#
+# Copyright 2010 Alexandre Fiori
+# based on the original Tornado by 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.
+
+import sys
+import os.path
+import cyclone.auth
+import cyclone.escape
+import cyclone.web
+from twisted.python import log
+from twisted.internet import reactor
+
+
+class Application(cyclone.web.Application):
+ def __init__(self):
+ handlers = [
+ (r"/", MainHandler),
+ (r"/auth/login", AuthLoginHandler),
+ (r"/auth/logout", AuthLogoutHandler),
+ ]
+ settings = dict(
+ cookie_secret="12oETzKXQAGaYdkL5gEmGeJJFuYh7EQnp2XdTP1o/Vo=",
+ login_url="/auth/login",
+ template_path=os.path.join(os.path.dirname(__file__), "templates"),
+ static_path=os.path.join(os.path.dirname(__file__), "static"),
+ xsrf_cookies=True,
+ facebook_api_key="501833353168262",
+ facebook_secret="56d788a22ee09499ea57b5044da123a1",
+ ui_modules={"Post": PostModule},
+ debug=True,
+ )
+ cyclone.web.Application.__init__(self, handlers, **settings)
+
+
+class BaseHandler(cyclone.web.RequestHandler):
+ def get_current_user(self):
+ user_json = self.get_secure_cookie("user")
+ if not user_json:
+ return None
+ return cyclone.escape.json_decode(user_json)
+
+
+class MainHandler(BaseHandler, cyclone.auth.FacebookGraphMixin):
+ @cyclone.web.authenticated
+ @cyclone.web.asynchronous
+ def get(self):
+ self.facebook_request("/me/friends", self._on_stream,
+ access_token=self.current_user["access_token"])
+
+ def _on_stream(self, stream):
+ if stream is None:
+ # Session may have expired
+ self.redirect("/auth/login")
+ return
+ self.render("stream.html", stream=stream)
+
+
+class AuthLoginHandler(BaseHandler, cyclone.auth.FacebookGraphMixin):
+ @cyclone.web.asynchronous
+ def get(self):
+ my_url = (self.request.protocol + "://" + self.request.host +
+ "/auth/login?next=" +
+ cyclone.escape.url_escape(self.get_argument("next", "/")))
+ if self.get_argument("code", False):
+ self.get_authenticated_user(
+ redirect_uri=my_url,
+ client_id=self.settings["facebook_api_key"],
+ client_secret=self.settings["facebook_secret"],
+ code=self.get_argument("code"),
+ callback=self._on_auth)
+ return
+ self.authorize_redirect(redirect_uri=my_url,
+ client_id=self.settings["facebook_api_key"],
+ extra_params={"scope": "read_stream"})
+
+ def _on_auth(self, user):
+ if not user:
+ raise cyclone.web.HTTPError(500, "Facebook auth failed")
+ self.set_secure_cookie("user", cyclone.escape.json_encode(user))
+ self.redirect(self.get_argument("next", "/"))
+
+
+class AuthLogoutHandler(BaseHandler, cyclone.auth.FacebookGraphMixin):
+ def get(self):
+ self.clear_cookie("user")
+ self.redirect(self.get_argument("next", "/"))
+
+
+class PostModule(cyclone.web.UIModule):
+ def render(self, post, actor):
+ return self.render_string("modules/post.html", post=post, actor=actor)
+
+
+def main():
+ reactor.listenTCP(8888, Application())
+ reactor.run()
+
+
+if __name__ == "__main__":
+ log.startLogging(sys.stdout)
+ main()
View
97 demos/fbgraphapi/static/facebook.css
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2009 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.
+ */
+
+body {
+ background: white;
+ color: black;
+ margin: 15px;
+}
+
+body,
+input,
+textarea {
+ font-family: "Lucida Grande", Tahoma, Verdana, sans-serif;
+ font-size: 10pt;
+}
+
+table {
+ border-collapse: collapse;
+ border: 0;
+}
+
+td {
+ border: 0;
+ padding: 0;
+}
+
+img {
+ border: 0;
+}
+
+a {
+ text-decoration: none;
+ color: #3b5998;
+}
+
+a:hover {
+ text-decoration: underline;
+}
+
+.post {
+ border-bottom: 1px solid #eeeeee;
+ min-height: 50px;
+ padding-bottom: 10px;
+ margin-top: 10px;
+}
+
+.post .picture {
+ float: left;
+}
+
+.post .picture img {
+ height: 50px;
+ width: 50px;
+}
+
+.post .body {
+ margin-left: 60px;
+}
+
+.post .media img {
+ border: 1px solid #cccccc;
+ padding: 3px;
+}
+
+.post .media:hover img {
+ border: 1px solid #3b5998;
+}
+
+.post a.actor {
+ font-weight: bold;
+}
+
+.post .meta {
+ font-size: 11px;
+}
+
+.post a.permalink {
+ color: #777777;
+}
+
+#body {
+ max-width: 700px;
+ margin: auto;
+}
View
0 demos/fbgraphapi/static/facebook.js
No changes.
View
17 demos/fbgraphapi/templates/modules/post.html
@@ -0,0 +1,17 @@
+<div class="post">
+ <div class="picture">
+ {% set author_url="http://www.facebook.com/profile.php?id=" + escape(post["from"]["id"]) %}
+ <a href="{{ author_url }}"><img src="//graph.facebook.com/{{ escape(post["from"]["id"]) }}/picture?type=square"/></a>
+ </div>
+ <div class="body">
+ <a href="{{ author_url }}" class="actor">{{ escape(post["from"]["name"]) }}</a>
+ {% if "message" in post %}
+ <span class="message">{{ escape(post["message"]) }}</span>
+ {% end %}
+ <div class="meta">
+ {% if "actions" in post %}
+ <a href="{{ escape(post["actions"][0]["link"]) }}" class="permalink">{{ locale.format_date(datetime.datetime.strptime(post["created_time"], "%Y-%m-%dT%H:%M:%S+0000")) }}</a>
+ {% end %}
+ </div>
+ </div>
+</div>
View
22 demos/fbgraphapi/templates/stream.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <title>Tornado Facebook Stream Demo</title>
+ <link rel="stylesheet" href="{{ static_url("facebook.css") }}" type="text/css"/>
+ </head>
+ <body>
+ <div id="body">
+ <div style="float:right">
+ <b>{{ escape(current_user["name"]) }}</b> -
+ <a href="/auth/logout">{{ _("Sign out") }}</a>
+ </div>
+ <div style="margin-bottom:1em"><a href="/">{{ _("Refresh stream") }}</a></div>
+ <div id="stream">
+ {% for post in stream["data"] %}
+ {{ modules.Post(post) }}
+ {% end %}
+ </div>
+ </div>
+ </body>
+</html>
View
24 demos/fbgraphapi/uimodules.py
@@ -0,0 +1,24 @@
+#!/usr/bin/env python
+# coding: utf-8
+#
+# Copyright 2010 Alexandre Fiori
+# based on the original Tornado by 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.
+
+import cyclone.web
+
+
+class Entry(cyclone.web.UIModule):
+ def render(self):
+ return '<div>ENTRY</div>'

1 comment on commit 25bba8d

@dpnova
Collaborator

log.warning is invalid here @sandsower

Please sign in to comment.