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()