Source code for oauth_dropins.blogger_v2
"""Blogger v2 GData API OAuth drop-in.
Blogger API docs:
https://developers.google.com/blogger/docs/2.0/developers_guide_protocol
Python GData API docs:
http://gdata-python-client.googlecode.com/hg/pydocs/gdata.blogger.data.html
Uses google-api-python-client to auth via OAuth 2. This describes how to get
gdata-python-client to use an OAuth 2 token from google-api-python-client:
http://blog.bossylobster.com/2012/12/bridging-oauth-20-objects-between-gdata.html#comment-form
Support was added to gdata-python-client here:
https://code.google.com/p/gdata-python-client/source/detail?r=ecb1d49b5fbe05c9bc6c8525e18812ccc02badc0
"""
import json
import logging
import re
import appengine_config
import googleplus
import handlers
import models
from webutil import util
from oauth2client.appengine import CredentialsModel, OAuth2Decorator, StorageByKeyName
from oauth2client.client import OAuth2Credentials
from gdata.blogger import client
from gdata import gauth
from google.appengine.ext import ndb
import httplib2
# global. initialized in StartHandler.to_path().
oauth_decorator = None
[docs]class BloggerV2Auth(models.BaseAuth):
"""An authenticated Blogger user.
Provides methods that return information about this user (or page) and make
OAuth-signed requests to the Blogger API. Stores OAuth credentials in the
datastore. See models.BaseAuth for usage details.
Blogger-specific details: implements http() and api() but not urlopen(). api()
returns a gdata.blogger.client.BloggerClient. The datastore entity key name is
the Blogger user id.
"""
name = ndb.StringProperty(required=True)
creds_json = ndb.TextProperty(required=True)
user_atom = ndb.TextProperty(required=True)
blogs_atom = ndb.TextProperty(required=True)
picture_url = ndb.TextProperty(required=True)
# the elements in both of these lists match
blog_ids = ndb.StringProperty(repeated=True)
blog_titles = ndb.StringProperty(repeated=True)
blog_hostnames = ndb.StringProperty(repeated=True)
[docs] def site_name(self):
return 'Blogger'
[docs] def user_display_name(self):
"""Returns the user's Blogger username.
"""
return self.name
[docs] def creds(self):
"""Returns an oauth2client.OAuth2Credentials.
"""
return OAuth2Credentials.from_json(self.creds_json)
[docs] def access_token(self):
"""Returns the OAuth access token string.
"""
return json.loads(self.creds_json)['access_token']
[docs] def http(self):
"""Returns an httplib2.Http that adds OAuth credentials to requests.
"""
http = httplib2.Http()
self.creds().authorize(http)
return http
[docs] @staticmethod
def api_from_creds(oauth2_creds):
"""Returns a gdata.blogger.client.BloggerClient.
Args:
oauth2_creds: OAuth2Credentials
"""
# this must be a client ie subclass of GDClient, since that's what
# OAuth2TokenFromCredentials.authorize() expects, *not* a service ie
# subclass of GDataService.
blogger = client.BloggerClient()
gauth.OAuth2TokenFromCredentials(oauth2_creds).authorize(blogger)
return blogger
def _api(self):
"""Returns a gdata.blogger.client.BloggerClient.
"""
return BloggerV2Auth.api_from_creds(self.creds())
# Wrapper classes around the StorageByKeyName and CredentialsModel model classes
# to change their kinds so we can store separate creds for Google+ and Blogger.
# Without this, after you've signed into one, signing into the other tries to
# reuse the existing creds without re-requesting access for the new product and
# scope, which obviously fails. I hoped approval_prompt=force or
# include_granted_scopes=true or both would fix this, but no luck. :/
[docs]class StorageByKeyName_Blogger(StorageByKeyName):
pass
[docs]class CredentialsModel_Blogger(CredentialsModel):
pass
[docs]class StartHandler(handlers.StartHandler, handlers.CallbackHandler):
"""Connects a Blogger account. Authenticates via OAuth.
"""
handle_exception = googleplus.handle_exception
# extracts the Blogger id from a profile URL
AUTHOR_URI_RE = re.compile(
r'.*(?:blogger\.com/(?:feeds|profile)|(?:plus|profiles)\.google\.com)/([0-9]+)(?:/blogs)')
# https://developers.google.com/blogger/docs/2.0/developers_guide_protocol#OAuth2Authorizing
# (the scope for the v3 API is https://www.googleapis.com/auth/blogger)
DEFAULT_SCOPE = 'https://www.blogger.com/feeds/'
[docs] @classmethod
def to(cls, to_path, scopes=None):
"""Override this since we need to_path to instantiate the oauth decorator.
"""
global oauth_decorator
if oauth_decorator is None:
oauth_decorator = OAuth2Decorator(
client_id=appengine_config.GOOGLE_CLIENT_ID,
client_secret=appengine_config.GOOGLE_CLIENT_SECRET,
scope=cls.make_scope_str(scopes),
callback_path=to_path,
approval_prompt='force',
# https://developers.google.com/accounts/docs/OAuth2WebServer#incrementalAuth
include_granted_scopes='true',
_storage_class=StorageByKeyName_Blogger,
_credentials_class=CredentialsModel_Blogger)
class Handler(cls):
@oauth_decorator.oauth_required
def post(self):
return self.get()
@oauth_decorator.oauth_required
def get(self):
state = self.request.get('state')
blogger = BloggerV2Auth.api_from_creds(oauth_decorator.credentials)
try:
blogs = blogger.get_blogs()
except BaseException, e:
# this api call often returns 401 Unauthorized for users who aren't
# signed up for blogger and/or don't have any blogs.
util.interpret_http_exception(e)
# we can't currently intercept declines for Google+ or Blogger, so the
# only time we return a None auth entity right now is on error.
self.finish(None, state=state)
return
for id in ([a.uri.text for a in blogs.author if a.uri] +
[l.href for l in blogs.link if l]):
if not id:
continue
match = self.AUTHOR_URI_RE.match(id)
if match:
id = match.group(1)
else:
logging.warning("Couldn't parse %s , using entire value as id", id)
break
blog_ids = []
blog_titles = []
blog_hostnames = []
for blog in blogs.entry:
blog_ids.append(blog.get_blog_id() or blog.get_blog_name())
blog_titles.append(blog.title.text)
blog_hostnames.append(util.domain_from_link(blog.GetHtmlLink().href)
if blog.GetHtmlLink() else None)
creds_json = oauth_decorator.credentials.to_json()
# extract profile picture URL
picture_url = None
for author in blogs.author:
for child in author.children:
if child.tag.split(':')[-1] == 'image':
picture_url = child.get_attributes('src')[0].value
break
auth = BloggerV2Auth(id=id,
name=author.name.text,
picture_url=picture_url,
creds_json=creds_json,
user_atom=str(author),
blogs_atom=str(blogs),
blog_ids=blog_ids,
blog_titles=blog_titles,
blog_hostnames=blog_hostnames)
auth.put()
self.finish(auth, state=state)
return Handler
[docs]class CallbackHandler(object):
"""OAuth callback handler factory.
"""
@staticmethod
def to(to_path):
StartHandler.to_path = to_path
global oauth_decorator
assert oauth_decorator
return oauth_decorator.callback_handler()