"""Disqus OAuth drop-in.
Disqus API docs: https://disqus.com/api/docs/
This drop-in is even more similar to Instagram than Instagram is to
Facebook. Differences:
- urlopen must pass the ``api_key`` with each request (in addition to the
``access_token``)
- Response to access_token does not give much information about the user,
so we additionally fetch ``/user/details`` before saving
- Deny appears to be broken on Disqus's side (clicking "No Thanks" has
no effect), so we ignore that possibility for now.
TODO: unify Disqus, Facebook, and Instagram
"""
import logging
import urllib.parse
from flask import request
from google.cloud import ndb
from . import models, views
from .webutil import flask_util, util
from .webutil.util import json_dumps, json_loads
logger = logging.getLogger(__name__)
DISQUS_CLIENT_ID = util.read('disqus_client_id')
DISQUS_CLIENT_SECRET = util.read('disqus_client_secret')
GET_AUTH_CODE_URL = (
'https://disqus.com/api/oauth/2.0/authorize/?' +
'&'.join((
'client_id=%(client_id)s',
'scope=%(scope)s',
'response_type=code',
'redirect_uri=%(redirect_uri)s',
)))
GET_ACCESS_TOKEN_URL = 'https://disqus.com/api/oauth/2.0/access_token/'
USER_DETAILS_URL = 'https://disqus.com/api/3.0/users/details.json?user=%d'
[docs]
class Start(views.Start):
"""Starts Disqus auth. Requests an auth code and expects a redirect back.
"""
NAME = 'disqus'
LABEL = 'Disqus'
# Disqus scopes are comma separated: read, write, admin, email
# https://disqus.com/api/docs/requests/#data-availability
DEFAULT_SCOPE = 'read'
[docs]
def redirect_url(self, state=None):
assert DISQUS_CLIENT_ID and DISQUS_CLIENT_SECRET, \
"Please fill in the disqus_client_id and disqus_client_secret files in your app's root directory."
return GET_AUTH_CODE_URL % {
'client_id': DISQUS_CLIENT_ID,
'scope': self.scope,
'redirect_uri': urllib.parse.quote_plus(self.to_url(state=state)),
}
[docs]
class Callback(views.Callback):
"""The auth callback. Fetches an access token, stores it, and redirects home.
"""
[docs]
def dispatch_request(self):
if self.handle_error():
return
# https://disqus.com/api/docs/auth/
auth_code = request.values['code']
data = {
'grant_type': 'authorization_code',
'client_id': DISQUS_CLIENT_ID,
'client_secret': DISQUS_CLIENT_SECRET,
'redirect_uri': self.request_url_with_state(),
'code': auth_code,
}
resp = util.requests_post(GET_ACCESS_TOKEN_URL, data=data)
resp.raise_for_status()
try:
data = json_loads(resp.text)
except (ValueError, TypeError):
logger.error(f'Bad response:\n{resp}', exc_info=True)
flask_util.error('Bad Disqus response to access token request')
access_token = data['access_token']
user_id = data['user_id']
# TODO is a username key preferred?
# username = data['username']
auth = DisqusAuth(id=str(user_id),
auth_code=auth_code,
access_token_str=access_token)
resp = auth.urlopen(USER_DETAILS_URL % user_id).read()
try:
user_data = json_loads(resp)['response']
except (ValueError, TypeError):
logger.error(f'Bad response:\n{resp}', exc_info=True)
flask_util.error('Bad Disqus response to user details request')
auth.user_json = json_dumps(user_data)
logger.info(f'created disqus auth {auth}')
auth.put()
return self.finish(auth, state=request.values.get('state'))
[docs]
def handle_error(handler):
"""Handles any error reported in the callback query parameters.
Args:
handler (Callback)
Returns:
bool: True if there was an error, False otherwise
"""
error = request.values.get('error')
if error:
if error == 'access_denied':
logger.info('User declined')
handler.finish(None, state=request.values.get('state'))
return True
else:
flask_util.error(error)
return False