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

try:
  from oauth2client.appengine import CredentialsModel, OAuth2Decorator, StorageByKeyName
except ImportError:
  from oauth2client.contrib.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 prompt=consent 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, prompt='consent', # 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()