def dispatch_request(self): features = request.form['feature'] scopes = PUBLISH_SCOPES if 'publish' in features else LISTEN_SCOPES starter = util.oauth_starter(oauth_github.Start, feature=features)('/github/add', scopes=scopes) return starter.dispatch_request()
def start_oauth_flow(self, feature): starter = util.oauth_starter( oauth_flickr.StartHandler, feature=feature ).to( '/flickr/add', scopes='write' if feature == 'publish' else 'read' ) return starter(self.request, self.response).post()
def post(self): features = util.get_required_param(self, 'feature') scopes = PUBLISH_SCOPES if 'publish' in features else LISTEN_SCOPES starter = util.oauth_starter(oauth_github.StartHandler, feature=features).to('/github/add', scopes=scopes) return starter(self.request, self.response).post()
def finish_oauth_flow(self, auth_entity, state): """Adds or deletes a FacebookPage, or restarts OAuth to get publish permissions. Args: auth_entity: FacebookAuth state: encoded state string """ if auth_entity is None: auth_entity_key = util.get_required_param(self, "auth_entity_key") auth_entity = ndb.Key(urlsafe=auth_entity_key).get() if state is None: state = self.request.get("state") state_obj = self.decode_state_parameter(state) id = state_obj.get("id") or self.request.get("id") if id and id != auth_entity.key.id(): auth_entity = auth_entity.for_page(id) auth_entity.put() source = self.maybe_add_or_delete_source(FacebookPage, auth_entity, state) # If we were already signed up for publish, we had an access token with publish # permissions. If we then go through the listen signup flow, we'll get a token # with just the listen permissions. In that case, do the whole OAuth flow again # to get a token with publish permissions again. feature = state_obj.get("feature") if source is not None and feature == "listen" and "publish" in source.features: logging.info("Restarting OAuth flow to get publish permissions.") source.features.remove("publish") source.put() start = util.oauth_starter(oauth_facebook.StartHandler, feature="publish", id=id) restart = start.to("/facebook/oauth_handler", scopes=PUBLISH_SCOPES) restart(self.request, self.response).post()
def post(self): features = self.request.get('feature') features = features.split(',') if features else [] starter = util.oauth_starter(oauth_facebook.StartHandler).to( '/facebook/oauth_handler', scopes=sorted(set( (LISTEN_SCOPES if 'listen' in features else []) + (PUBLISH_SCOPES if 'publish' in features else [])))) starter(self.request, self.response).post()
def post(self): features = self.request.get('feature') features = features.split(',') if features else [] starter = util.oauth_starter(oauth_instagram.StartHandler).to( '/instagram/oauth_callback', # http://instagram.com/developer/authentication/#scope scopes='likes comments' if 'publish' in features else None) starter(self.request, self.response).post()
def post(self): # pass explicit 'write' instead of None for publish so that oauth-dropins # (and tweepy) don't use signin_with_twitter ie /authorize. this works # around a twitter API bug: https://dev.twitter.com/discussions/21281 access_type = ('read' if util.get_required_param(self, 'feature') == 'listen' else 'write') handler = util.oauth_starter(oauth_twitter.StartHandler).to( '/twitter/add', access_type=access_type)(self.request, self.response) return handler.post()
def post(self): features = self.request.get("feature") features = features.split(",") if features else [] starter = util.oauth_starter(oauth_facebook.StartHandler).to( "/facebook/oauth_handler", scopes=sorted( set((LISTEN_SCOPES if "listen" in features else []) + (PUBLISH_SCOPES if "publish" in features else [])) ), ) starter(self.request, self.response).post()
def post(self): ia_start = util.oauth_starter(indieauth.StartHandler).to('/instagram/callback')( self.request, self.response) try: self.redirect(ia_start.redirect_url(me=util.get_required_param(self, 'user_url'))) except Exception as e: if util.is_connection_failure(e) or util.interpret_http_exception(e)[0]: self.messages.add("Couldn't fetch your web site: %s" % e) return self.redirect('/') raise
def start_oauth_flow(self, feature): starter = util.oauth_starter(oauth_flickr.Start, feature=feature)( # TODO: delete instead of write. if we do that below, it works, and we get # granted delete permissions. however, if we then attempt to actually # delete something, it fails with code 99 "Insufficient permissions. # Method requires delete privileges; write granted." and # https://www.flickr.com/services/auth/list.gne shows that my user's # permissions for the Bridgy app are back to write, not delete. wtf?! '/flickr/add', scopes='write' if feature == 'publish' else 'read') return starter.dispatch_request()
def post(self): features = self.request.get('feature') features = features.split(',') if features else [] callback = util.get_required_param(self, 'callback') ia_start = util.oauth_starter( indieauth.StartHandler).to('/instagram/callback')(self.request, self.response) self.redirect( ia_start.redirect_url( me=util.get_required_param(self, 'user_url')))
def start_oauth_flow(self, feature): starter = util.oauth_starter( oauth_flickr.StartHandler, feature=feature ).to( # TODO: delete instead of write. if we do that below, it works, and we get # granted delete permissions. however, if we then attempt to actually # delete something, it fails with code 99 "Insufficient permissions. # Method requires delete privileges; write granted." and # https://www.flickr.com/services/auth/list.gne shows that my user's # permissions for the Bridgy app are back to write, not delete. wtf?! '/flickr/add', scopes='write' if feature == 'publish' else 'read' ) return starter(self.request, self.response).post()
def post(self): feature = self.request.get('feature') start_cls = util.oauth_starter(StartHandler).to('/mastodon/callback', scopes=PUBLISH_SCOPES if feature == 'publish' else LISTEN_SCOPES) start = start_cls(self.request, self.response) instance = util.get_required_param(self, 'instance') try: self.redirect(start.redirect_url(instance=instance)) except ValueError as e: logging.warning('Bad Mastodon instance', exc_info=True) self.messages.add(util.linkify(unicode(e), pretty=True)) return self.redirect(self.request.path)
def redirect_url(self, *args, **kwargs): features = (request.form.get('feature') or '').split(',') starter = util.oauth_starter(StartBase)( '/mastodon/callback', scopes=PUBLISH_SCOPES if 'publish' in features else LISTEN_SCOPES) try: return starter.redirect_url(*args, instance=request.form['instance'], **kwargs) except ValueError as e: logger.warning('Bad Mastodon instance', exc_info=True) flash(util.linkify(str(e), pretty=True)) redirect(request.path)
def finish_oauth_flow(self, auth_entity, state): """Adds or deletes a :class:`FacebookPage`, or restarts OAuth to get publish permissions. Args: auth_entity: :class:`oauth_dropins.facebook.FacebookAuth` state: encoded state string """ if auth_entity is None: auth_entity_key = self.request.get('auth_entity_key') if auth_entity_key: auth_entity = ndb.Key(urlsafe=auth_entity_key).get() if state is None: state = self.request.get('state') state_obj = util.decode_oauth_state(state) if state else {} id = state_obj.get('id') or self.request.get('id') if id and auth_entity and id != auth_entity.key.id(): auth_entity = auth_entity.for_page(id) if auth_entity: auth_entity.put() source = self.maybe_add_or_delete_source(FacebookPage, auth_entity, state) # If we were already signed up for publish, we had an access token with publish # permissions. If we then go through the listen signup flow, we'll get a token # with just the listen permissions. In that case, do the whole OAuth flow again # to get a token with publish permissions again. feature = state_obj.get('feature') if source is not None and feature == 'listen' and 'publish' in source.features: logging.info('Restarting OAuth flow to get publish permissions.') source.features.remove('publish') source.put() start = util.oauth_starter(oauth_facebook.StartHandler, feature='publish', id=id) restart = start.to('/facebook/oauth_handler', scopes=PUBLISH_SCOPES) restart(self.request, self.response).post() # ask the user for their web site if we don't already have one. if source and not source.domains: self.redirect( '/edit-websites?' + urllib.parse.urlencode({ 'source_key': source.key.urlsafe(), }))
def start_oauth_flow(self, feature): """Redirects to Twitter's OAuth endpoint to start the OAuth flow. Args: feature: 'listen' or 'publish' """ features = feature.split(',') if feature else [] assert all(f in models.Source.FEATURES for f in features) # pass explicit 'write' instead of None for publish so that oauth-dropins # (and tweepy) don't use signin_with_twitter ie /authorize. this works # around a twitter API bug: https://dev.twitter.com/discussions/21281 access_type = 'write' if 'publish' in features else 'read' handler = util.oauth_starter(oauth_twitter.StartHandler, feature=feature).to( '/twitter/add', access_type=access_type)(self.request, self.response) return handler.post()
def start_oauth_flow(self, feature, operation): """Redirects to Reddit's OAuth endpoint to start the OAuth flow. Args: feature: 'listen' or 'publish', only 'listen' supported """ features = feature.split(',') if feature else [] for feature in features: if feature not in models.Source.FEATURES: raise exc.HTTPBadRequest('Unknown feature: %s' % feature) handler = util.oauth_starter( oauth_reddit.StartHandler, feature=feature, operation=operation).to('/reddit/callback')(self.request, self.response) return handler.post()
def start_oauth_flow(self, feature): """Redirects to Twitter's OAuth endpoint to start the OAuth flow. Args: feature: 'listen' or 'publish' """ features = feature.split(',') if feature else [] for feature in features: if feature not in models.Source.FEATURES: util.error(f'Unknown feature: {feature}') # pass explicit 'write' instead of None for publish so that oauth-dropins # (and tweepy) don't use signin_with_twitter ie /authorize. this works # around a twitter API bug: https://dev.twitter.com/discussions/21281 access_type = 'write' if 'publish' in features else 'read' view = util.oauth_starter(oauth_twitter.Start, feature=feature)( '/twitter/add', access_type=access_type) return view.dispatch_request()
def start_oauth_flow(self, feature): """Redirects to Twitter's OAuth endpoint to start the OAuth flow. Args: feature: 'listen' or 'publish' """ features = feature.split(',') if feature else [] for feature in features: if feature not in models.Source.FEATURES: raise exc.HTTPBadRequest('Unknown feature: %s' % feature) # pass explicit 'write' instead of None for publish so that oauth-dropins # (and tweepy) don't use signin_with_twitter ie /authorize. this works # around a twitter API bug: https://dev.twitter.com/discussions/21281 access_type = 'write' if 'publish' in features else 'read' handler = util.oauth_starter(oauth_twitter.StartHandler, feature=feature).to( '/twitter/add', access_type=access_type)(self.request, self.response) return handler.post()
def finish_oauth_flow(self, auth_entity, state): """Adds or deletes a :class:`FacebookPage`, or restarts OAuth to get publish permissions. Args: auth_entity: :class:`oauth_dropins.facebook.FacebookAuth` state: encoded state string """ if auth_entity is None: auth_entity_key = self.request.get('auth_entity_key') if auth_entity_key: auth_entity = ndb.Key(urlsafe=auth_entity_key).get() if state is None: state = self.request.get('state') state_obj = util.decode_oauth_state(state) if state else {} id = state_obj.get('id') or self.request.get('id') if id and auth_entity and id != auth_entity.key.id(): auth_entity = auth_entity.for_page(id) if auth_entity: auth_entity.put() source = self.maybe_add_or_delete_source(FacebookPage, auth_entity, state) # If we were already signed up for publish, we had an access token with publish # permissions. If we then go through the listen signup flow, we'll get a token # with just the listen permissions. In that case, do the whole OAuth flow again # to get a token with publish permissions again. feature = state_obj.get('feature') if source is not None and feature == 'listen' and 'publish' in source.features: logging.info('Restarting OAuth flow to get publish permissions.') source.features.remove('publish') source.put() start = util.oauth_starter(oauth_facebook.StartHandler, feature='publish', id=id) restart = start.to('/facebook/oauth_handler', scopes=PUBLISH_SCOPES) restart(self.request, self.response).post() # ask the user for their web site if we don't already have one. if source and not source.domains: self.redirect('/edit-websites?' + urllib.urlencode({ 'source_key': source.key.urlsafe(), }))
"""Returns the GitHub account URL, e.g. https://github.com/foo.""" return self.gr_source.user_url(self.key.id()) def label_name(self): """Returns the username.""" return self.key.id() def get_activities_response(self, *args, **kwargs): """Drop kwargs that granary doesn't currently support for github.""" kwargs.update({ 'fetch_shares': None, 'fetch_mentions': None, }) return self.gr_source.get_activities_response(*args, **kwargs) class AddGitHub(oauth_github.CallbackHandler, util.Handler): def finish(self, auth_entity, state=None): logging.debug('finish with %s, %s', auth_entity, state) self.maybe_add_or_delete_source(GitHub, auth_entity, state) application = webapp2.WSGIApplication([ ('/github/start', util.oauth_starter(oauth_github.StartHandler).to( '/github/add', scopes=LISTEN_SCOPES)), ('/github/add', AddGitHub), ('/github/delete/finish', oauth_github.CallbackHandler.to('/delete/finish')), ('/github/publish/start', oauth_github.StartHandler.to( '/publish/github/finish', scopes=PUBLISH_SCOPES)), ], debug=appengine_config.DEBUG)
self.response.headers['Content-Type'] = 'text/html' self.response.out.write(template.render('templates/choose_blog.html', vars)) class AddTumblr(util.Handler): def post(self): auth_entity_key = util.get_required_param(self, 'auth_entity_key') self.maybe_add_or_delete_source( Tumblr, ndb.Key(urlsafe=auth_entity_key).get(), util.get_required_param(self, 'state'), blog_name=util.get_required_param(self, 'blog'), ) class SuperfeedrNotifyHandler(superfeedr.NotifyHandler): SOURCE_CLS = Tumblr application = webapp2.WSGIApplication([ # Tumblr doesn't seem to use scope # http://www.tumblr.com/docs/en/api/v2#oauth ('/tumblr/start', util.oauth_starter(oauth_tumblr.StartHandler).to( '/tumblr/choose_blog')), ('/tumblr/choose_blog', ChooseBlog), ('/tumblr/add', AddTumblr), ('/tumblr/delete/finish', oauth_tumblr.CallbackHandler.to('/delete/finish')), ('/tumblr/notify/(.+)', SuperfeedrNotifyHandler), ], debug=appengine_config.DEBUG)
@app.route('/blogger/add', methods=['POST']) def blogger_add(): util.maybe_add_or_delete_source( Blogger, ndb.Key(urlsafe=request.form['auth_entity_key']).get(), request.form['state'], blog_id=request.form['blog'], ) class SuperfeedrNotify(superfeedr.Notify): SOURCE_CLS = Blogger # Blogger only has one OAuth scope. oauth-dropins fills it in. # https://developers.google.com/blogger/docs/2.0/developers_guide_protocol#OAuth2Authorizing start = util.oauth_starter(oauth_blogger.Start).as_view( 'blogger_start', '/blogger/oauth2callback') app.add_url_rule('/blogger/start', view_func=start, methods=['POST']) app.add_url_rule('/blogger/oauth2callback', view_func=oauth_blogger.Callback.as_view( 'blogger_oauth2callback', '/blogger/oauth_handler')) app.add_url_rule('/blogger/delete/start', view_func=oauth_blogger.Start.as_view( 'blogger_delete_start', '/blogger/oauth2callback')) app.add_url_rule('/blogger/notify/<id>', view_func=SuperfeedrNotify.as_view('blogger_notify'), methods=['POST'])
self.response.headers['Content-Type'] = 'text/html' self.response.out.write(JINJA_ENV.get_template('choose_blog.html').render(**vars)) class AddTumblr(util.Handler): def post(self): auth_entity_key = util.get_required_param(self, 'auth_entity_key') self.maybe_add_or_delete_source( Tumblr, ndb.Key(urlsafe=auth_entity_key).get(), util.get_required_param(self, 'state'), blog_name=util.get_required_param(self, 'blog'), ) class SuperfeedrNotifyHandler(superfeedr.NotifyHandler): SOURCE_CLS = Tumblr application = webapp2.WSGIApplication([ # Tumblr doesn't seem to use scope # http://www.tumblr.com/docs/en/api/v2#oauth ('/tumblr/start', util.oauth_starter(oauth_tumblr.StartHandler).to( '/tumblr/choose_blog')), ('/tumblr/choose_blog', ChooseBlog), ('/tumblr/add', AddTumblr), ('/tumblr/delete/finish', oauth_tumblr.CallbackHandler.to('/delete/finish')), ('/tumblr/notify/(.+)', SuperfeedrNotifyHandler), ], debug=appengine_config.DEBUG)
'title': p.get('name', ''), 'url': p.get('url', ''), 'pretty_url': util.pretty_link(str(p.get('url', ''))), 'image': p.get('imageUrl', ''), } for p in pubs if p.get('id')], } logger.info(f'Rendering choose_blog.html with {vars}') return render_template('choose_blog.html', **vars) class SuperfeedrNotify(superfeedr.Notify): SOURCE_CLS = Medium # https://github.com/Medium/medium-api-docs#user-content-21-browser-based-authentication start = util.oauth_starter(oauth_medium.Start).as_view( 'medium_start', '/medium/choose_blog', scopes=('basicProfile', 'listPublications')) app.add_url_rule('/medium/start', view_func=start, methods=['POST']) app.add_url_rule('/medium/choose_blog', view_func=ChooseBlog.as_view('medium_choose_blog', 'unused to_path'), methods=['GET']) app.add_url_rule('/medium/delete/finish', view_func=oauth_medium.Callback.as_view( 'medium_delete', '/delete/finish')), app.add_url_rule('/medium/notify/<id>', view_func=SuperfeedrNotify.as_view('medium_notify'), methods=['POST'])
if site_info is None: return elif site_info.get('jetpack'): logger.info(f'This is a self-hosted WordPress blog! {auth_entity.key_id()} {auth_entity.blog_id}') return render_template('confirm_self_hosted_wordpress.html', auth_entity_key=auth_entity.key.urlsafe().decode(), state=state) util.maybe_add_or_delete_source(WordPress, auth_entity, state) @app.route('/wordpress/confirm', methods=['POST']) def confirm_self_hosted(): util.maybe_add_or_delete_source( WordPress, ndb.Key(urlsafe=request.form['auth_entity_key']).get(), request.form['state']) class SuperfeedrNotify(superfeedr.Notify): SOURCE_CLS = WordPress # wordpress.com doesn't seem to use scope # https://developer.wordpress.com/docs/oauth2/ start = util.oauth_starter(oauth_wordpress.Start).as_view( 'wordpress_start', '/wordpress/add') app.add_url_rule('/wordpress/start', view_func=start, methods=['POST']) app.add_url_rule('/wordpress/add', view_func=Add.as_view('wordpress_add', 'unused')) app.add_url_rule('/wordpress/notify/<id>', view_func=SuperfeedrNotify.as_view('wordpress_notify'), methods=['POST'])
# matter for now since we don't plan to implement publish for G+. state = self.construct_state_param_for_add(feature='listen') auth_entity = ndb.Key(urlsafe=auth_entity_str_key).get() self.maybe_add_or_delete_source(GooglePlusPage, auth_entity, state) application = webapp2.WSGIApplication( [ # OAuth scopes based on https://developers.google.com/+/api/oauth#scopes # # Used to use plus.login, but that showed a scary-ish warning on the consent # screen: # "Know the list of people in your circles, your age range, and language... # [in red:] Includes people in circles that are not public on your profile." # # Switched to plus.me (below), which grants less info, but still works fine # for what we need. activities.get and .search don't say they need anything, # and .list says both plus.login and plus.me work (as of 2016-08-13): # https://developers.google.com/+/web/api/rest/latest/activities/list ('/googleplus/start', util.oauth_starter( oauth_googleplus.StartHandler).to( '/googleplus/oauth2callback', scopes='https://www.googleapis.com/auth/plus.me')), ('/googleplus/oauth2callback', oauth_googleplus.CallbackHandler.to('/googleplus/add')), ('/googleplus/add', OAuthCallback), ('/googleplus/delete/start', oauth_googleplus.StartHandler.to('/googleplus/oauth2callback')), ], debug=appengine_config.DEBUG)
def label_name(self): """Returns the username.""" return self.key.id() def get_activities_response(self, *args, **kwargs): """Drop kwargs that granary doesn't currently support for github.""" kwargs.update({ 'fetch_shares': None, 'fetch_mentions': None, }) return self.gr_source.get_activities_response(*args, **kwargs) class AddGitHub(oauth_github.CallbackHandler, util.Handler): def finish(self, auth_entity, state=None): logging.debug('finish with %s, %s', auth_entity, state) self.maybe_add_or_delete_source(GitHub, auth_entity, state) ROUTES = [ ('/github/start', util.oauth_starter(oauth_github.StartHandler).to('/github/add', scopes=LISTEN_SCOPES)), ('/github/add', AddGitHub), ('/github/delete/finish', oauth_github.CallbackHandler.to('/delete/finish')), ('/github/publish/start', oauth_github.StartHandler.to('/publish/github/finish', scopes=PUBLISH_SCOPES)), ]
self.response.headers['Content-Type'] = 'text/html' self.response.out.write(template.render('templates/choose_blog.html', vars)) class AddBlogger(util.Handler): def post(self): auth_entity_key = util.get_required_param(self, 'auth_entity_key') self.maybe_add_or_delete_source( Blogger, ndb.Key(urlsafe=auth_entity_key).get(), util.get_required_param(self, 'state'), blog_id=util.get_required_param(self, 'blog'), ) class SuperfeedrNotifyHandler(superfeedr.NotifyHandler): SOURCE_CLS = Blogger application = webapp2.WSGIApplication([ # Blogger only has one OAuth scope. oauth-dropins fills it in. # https://developers.google.com/blogger/docs/2.0/developers_guide_protocol#OAuth2Authorizing ('/blogger/start', util.oauth_starter(oauth_blogger.StartHandler).to( '/blogger/oauth2callback')), ('/blogger/oauth2callback', oauth_blogger.CallbackHandler.to('/blogger/oauth_handler')), ('/blogger/oauth_handler', OAuthCallback), ('/blogger/add', AddBlogger), ('/blogger/delete/start', oauth_blogger.StartHandler.to('/blogger/oauth2callback')), ('/blogger/notify/(.+)', SuperfeedrNotifyHandler), ], debug=appengine_config.DEBUG)
self.maybe_add_or_delete_source(WordPress, auth_entity, state) class ConfirmSelfHosted(util.Handler): def post(self): self.maybe_add_or_delete_source( WordPress, ndb.Key(urlsafe=util.get_required_param(self, 'auth_entity_key')).get(), util.get_required_param(self, 'state')) class SuperfeedrNotifyHandler(superfeedr.NotifyHandler): SOURCE_CLS = WordPress application = webapp2.WSGIApplication( [ # wordpress.com doesn't seem to use scope # https://developer.wordpress.com/docs/oauth2/ ('/wordpress/start', util.oauth_starter( oauth_wordpress.StartHandler).to('/wordpress/add')), ('/wordpress/confirm', ConfirmSelfHosted), # This handles both add and delete. (WordPress.com only allows a single # OAuth redirect URL.) ('/wordpress/add', AddWordPress), ('/wordpress/notify/(.+)', SuperfeedrNotifyHandler), ], debug=appengine_config.DEBUG)
class OAuthCallback(util.Handler): """OAuth callback handler. Both the add and delete flows have to share this because Google+'s oauth-dropin doesn't yet allow multiple callback handlers. :/ """ def get(self): auth_entity_str_key = util.get_required_param(self, "auth_entity") state = self.request.get("state") if not state: # state doesn't currently come through for G+. not sure why. doesn't # matter for now since we don't plan to implement publish for G+. state = self.construct_state_param_for_add(feature="listen") auth_entity = ndb.Key(urlsafe=auth_entity_str_key).get() self.maybe_add_or_delete_source(GooglePlusPage, auth_entity, state) application = webapp2.WSGIApplication( [ # OAuth scopes are set in listen.html and publish.html ("/googleplus/start", util.oauth_starter(oauth_googleplus.StartHandler).to("/googleplus/oauth2callback")), ("/googleplus/oauth2callback", oauth_googleplus.CallbackHandler.to("/googleplus/add")), ("/googleplus/add", OAuthCallback), ("/googleplus/delete/start", oauth_googleplus.StartHandler.to("/googleplus/oauth2callback")), ], debug=appengine_config.DEBUG, )
'id': p['id'], 'title': p.get('name', ''), 'url': p.get('url', ''), 'pretty_url': util.pretty_link(str(p.get('url', ''))), 'image': p.get('imageUrl', ''), } for p in pubs if p.get('id')], } logging.info('Rendering choose_blog.html with %s', vars) self.response.headers['Content-Type'] = 'text/html' self.response.out.write( JINJA_ENV.get_template('choose_blog.html').render(**vars)) class SuperfeedrNotifyHandler(superfeedr.NotifyHandler): SOURCE_CLS = Medium application = webapp2.WSGIApplication( [ # https://github.com/Medium/medium-api-docs#user-content-21-browser-based-authentication ('/medium/start', util.oauth_starter(oauth_medium.StartHandler).to( '/medium/choose_blog', scopes=('basicProfile', 'listPublications')) ), ('/medium/add', AddMedium), ('/medium/choose_blog', ChooseBlog), ('/medium/delete/finish', oauth_medium.CallbackHandler.to('/delete/finish')), ('/medium/notify/(.+)', SuperfeedrNotifyHandler), ], debug=appengine_config.DEBUG)
vars = { 'action': '/medium/add', 'state': state, 'auth_entity_key': auth_entity.key.urlsafe(), 'blogs': [{ 'id': p['id'], 'title': p.get('name', ''), 'url': p.get('url', ''), 'pretty_url': util.pretty_link(str(p.get('url', ''))), 'image': p.get('imageUrl', ''), } for p in pubs if p.get('id')], } logging.info('Rendering choose_blog.html with %s', vars) self.response.headers['Content-Type'] = 'text/html' self.response.out.write(JINJA_ENV.get_template('choose_blog.html').render(**vars)) class SuperfeedrNotifyHandler(superfeedr.NotifyHandler): SOURCE_CLS = Medium application = webapp2.WSGIApplication([ # https://github.com/Medium/medium-api-docs#user-content-21-browser-based-authentication ('/medium/start', util.oauth_starter(oauth_medium.StartHandler).to( '/medium/choose_blog', scopes=('basicProfile', 'listPublications'))), ('/medium/add', AddMedium), ('/medium/choose_blog', ChooseBlog), ('/medium/delete/finish', oauth_medium.CallbackHandler.to('/delete/finish')), ('/medium/notify/(.+)', SuperfeedrNotifyHandler), ], debug=appengine_config.DEBUG)
raise models.DisableSource() def search_for_links(self): """Searches for activities with links to any of this source's web sites. Returns: sequence of ActivityStreams activity dicts """ urls = {util.schemeless(util.fragmentless(url), slashes=False) for url in self.domain_urls if not util.in_webmention_blocklist(util.domain_from_link(url))} if not urls: return [] # Search syntax: https://www.reddit.com/wiki/search url_query = ' OR '.join(f'site:"{u}" OR selftext:"{u}"' for u in urls) return self.get_activities( search_query=url_query, group_id=gr_source.SEARCH, etag=self.last_activities_etag, fetch_replies=False, fetch_likes=False, fetch_shares=False, count=50) class Callback(oauth_reddit.Callback): def finish(self, auth_entity, state=None): util.maybe_add_or_delete_source(Reddit, auth_entity, state) app.add_url_rule('/reddit/start', view_func=util.oauth_starter(oauth_reddit.Start).as_view('reddit_start', '/reddit/callback'), methods=['POST']) app.add_url_rule('/reddit/callback', view_func=Callback.as_view('reddit_callback', 'unused to_path'))
self.maybe_add_or_delete_source(WordPress, auth_entity, state) class ConfirmSelfHosted(util.Handler): def post(self): self.maybe_add_or_delete_source( WordPress, ndb.Key(urlsafe=util.get_required_param(self, "auth_entity_key")).get(), util.get_required_param(self, "state"), ) class SuperfeedrNotifyHandler(superfeedr.NotifyHandler): SOURCE_CLS = WordPress application = webapp2.WSGIApplication( [ # wordpress.com doesn't seem to use scope # https://developer.wordpress.com/docs/oauth2/ ("/wordpress/start", util.oauth_starter(oauth_wordpress.StartHandler).to("/wordpress/add")), ("/wordpress/confirm", ConfirmSelfHosted), # This handles both add and delete. (WordPress.com only allows a single # OAuth redirect URL.) ("/wordpress/add", AddWordPress), ("/wordpress/notify/(.+)", SuperfeedrNotifyHandler), ], debug=appengine_config.DEBUG, )
self.response.headers['Content-Type'] = 'text/html' self.response.out.write( template.render('templates/choose_facebook.html', vars)) class AddFacebookPage(util.Handler): def post(self): state = util.get_required_param(self, 'state') id = util.get_required_param(self, 'id') auth_entity_key = util.get_required_param(self, 'auth_entity_key') auth_entity = ndb.Key(urlsafe=auth_entity_key).get() if id != auth_entity.key.id(): auth_entity = auth_entity.for_page(id) auth_entity.put() self.maybe_add_or_delete_source(FacebookPage, auth_entity, state) application = webapp2.WSGIApplication([ # OAuth scopes are set in listen.html and publish.html ('/facebook/start', util.oauth_starter(oauth_facebook.StartHandler).to( '/facebook/oauth_handler')), ('/facebook/oauth_handler', OAuthCallback), ('/facebook/add', AddFacebookPage), ('/facebook/delete/finish', oauth_facebook.CallbackHandler.to('/delete/finish')), ('/facebook/publish/start', oauth_facebook.StartHandler.to( '/publish/facebook/finish')), ], debug=appengine_config.DEBUG)
return render_template('choose_blog.html', **vars) @app.route('/tumblr/add', methods=['POST']) def tumblr_add(): util.maybe_add_or_delete_source( Tumblr, ndb.Key(urlsafe=request.form['auth_entity_key']).get(), request.form['state'], blog_name=request.form['blog'], ) class SuperfeedrNotify(superfeedr.Notify): SOURCE_CLS = Tumblr # Tumblr doesn't seem to use scope # http://www.tumblr.com/docs/en/api/v2#oauth start = util.oauth_starter(oauth_tumblr.Start).as_view('tumblr_start', '/tumblr/choose_blog') app.add_url_rule('/tumblr/start', view_func=start, methods=['POST']) app.add_url_rule('/tumblr/choose_blog', view_func=ChooseBlog.as_view('tumblr_choose_blog', 'unused')) app.add_url_rule('/tumblr/delete/finish', view_func=oauth_tumblr.Callback.as_view( 'tumblr_delete_finish', '/delete/finish')) app.add_url_rule('/tumblr/notify/<id>', view_func=SuperfeedrNotify.as_view('tumblr_notify'), methods=['POST'])
is http://instagram.com """ return super(Instagram, self).canonicalize_syndication_url( syndication_url, scheme='http') class OAuthCallback(oauth_instagram.CallbackHandler, util.Handler): """OAuth callback handler. The add, delete, and interactive publish flows have to share this because Instagram only allows a single callback URL per app. :/ """ def finish(self, auth_entity, state=None): if 'target_url' in self.decode_state_parameter(state): # this is an interactive publish return self.redirect(util.add_query_params( '/publish/instagram/finish', util.trim_nulls({'auth_entity': auth_entity.key.urlsafe(), 'state': state}))) self.maybe_add_or_delete_source(Instagram, auth_entity, state) application = webapp2.WSGIApplication([ ('/instagram/start', util.oauth_starter(oauth_instagram.StartHandler).to( '/instagram/oauth_callback')), ('/instagram/publish/start', oauth_instagram.StartHandler.to( '/instagram/oauth_callback')), ('/instagram/oauth_callback', OAuthCallback), ], debug=appengine_config.DEBUG)
self.response.headers['Content-Type'] = 'text/html' self.response.out.write(template.render( 'templates/confirm_self_hosted_wordpress.html', {'auth_entity_key': auth_entity.key.urlsafe(), 'state': state})) return self.maybe_add_or_delete_source(WordPress, auth_entity, state) class ConfirmSelfHosted(util.Handler): def post(self): self.maybe_add_or_delete_source( WordPress, ndb.Key(urlsafe=util.get_required_param(self, 'auth_entity_key')).get(), util.get_required_param(self, 'state')) class SuperfeedrNotifyHandler(superfeedr.NotifyHandler): SOURCE_CLS = WordPress application = webapp2.WSGIApplication([ ('/wordpress/start', util.oauth_starter(oauth_wordpress.StartHandler).to( '/wordpress/add')), ('/wordpress/confirm', ConfirmSelfHosted), # This handles both add and delete. (WordPress.com only allows a single # OAuth redirect URL.) ('/wordpress/add', AddWordPress), ('/wordpress/notify/(.+)', SuperfeedrNotifyHandler), ], debug=appengine_config.DEBUG)
""" return super(GooglePlusPage, self).canonicalize_syndication_url( util.follow_redirects(url).url) class OAuthCallback(util.Handler): """OAuth callback handler. Both the add and delete flows have to share this because Google+'s oauth-dropin doesn't yet allow multiple callback handlers. :/ """ def get(self): auth_entity_str_key = util.get_required_param(self, 'auth_entity') state = self.request.get('state') if not state: # state doesn't currently come through for G+. not sure why. doesn't # matter for now since we don't plan to implement publish for G+. state = self.construct_state_param_for_add(feature='listen') auth_entity = ndb.Key(urlsafe=auth_entity_str_key).get() self.maybe_add_or_delete_source(GooglePlusPage, auth_entity, state) application = webapp2.WSGIApplication([ # OAuth scopes are set in listen.html and publish.html ('/googleplus/start', util.oauth_starter(oauth_googleplus.StartHandler).to( '/googleplus/oauth2callback')), ('/googleplus/oauth2callback', oauth_googleplus.CallbackHandler.to('/googleplus/add')), ('/googleplus/add', OAuthCallback), ('/googleplus/delete/start', oauth_googleplus.StartHandler.to('/googleplus/oauth2callback')), ], debug=appengine_config.DEBUG)
etag=self.last_activities_etag, fetch_replies=False, fetch_likes=False, fetch_shares=False, count=50) return [] class OAuthCallback(util.Handler): """OAuth callback handler. Both the add and delete flows have to share this because Google+'s oauth-dropin doesn't yet allow multiple callback handlers. :/ """ def get(self): auth_entity_str_key = util.get_required_param(self, 'auth_entity') state = self.request.get('state') if not state: # state doesn't currently come through for G+. not sure why. doesn't # matter for now since we don't plan to implement publish for G+. state = self.construct_state_param_for_add(feature='listen') auth_entity = ndb.Key(urlsafe=auth_entity_str_key).get() self.maybe_add_or_delete_source(GooglePlusPage, auth_entity, state) application = webapp2.WSGIApplication([ # OAuth scopes based on https://developers.google.com/+/api/oauth#scopes ('/googleplus/start', util.oauth_starter(oauth_googleplus.StartHandler).to( '/googleplus/oauth2callback', scopes='https://www.googleapis.com/auth/plus.login')) , ('/googleplus/oauth2callback', oauth_googleplus.CallbackHandler.to('/googleplus/add')), ('/googleplus/add', OAuthCallback), ('/googleplus/delete/start', oauth_googleplus.StartHandler.to('/googleplus/oauth2callback')), ], debug=appengine_config.DEBUG)
@staticmethod def _images(obj): return [image['url'] for image in util.get_list(obj, 'image')] class OAuthStart(oauth_views.Start): """Stand-in for the oauth-dropins Start, redirects to a made-up silo url.""" def redirect_url(self, state=None): logger.debug(f'oauth view redirect with state {state}') return 'http://fake/auth/url?' + urllib.parse.urlencode({ 'redirect_uri': self.to_url(state), }) FakeStart = util.oauth_starter(OAuthStart) class FakeSource(Source): GR_CLASS = FakeGrSource OAUTH_START = FakeStart SHORT_NAME = 'fake' TYPE_LABELS = {'post': 'FakeSource post label'} RATE_LIMITED_POLL = timedelta(hours=30) URL_CANONICALIZER = util.UrlCanonicalizer(domain=GR_CLASS.DOMAIN) PATH_BLOCKLIST = (re.compile('^/blocklisted/.*'), ) HAS_BLOCKS = True string_id_counter = 1 gr_source = FakeGrSource() is_saved = False
sent=['http://a/link'], )] class OAuthStartHandler(oauth_handlers.StartHandler): """Stand-in for the oauth-dropins StartHandler, redirects to a made-up silo url """ def redirect_url(self, state=None): logging.debug('oauth handler redirect') return 'http://fake/auth/url?' + urllib.urlencode({ 'redirect_uri': self.to_url(state), }) FakeStartHandler = util.oauth_starter(OAuthStartHandler).to('/fakesource/add') class FakeAddHandler(util.Handler): """Handles the authorization callback when handling a fake source """ auth_entity = FakeAuthEntity(user_json=json.dumps({ 'id': '0123456789', 'name': 'Fake User', 'url': 'http://fakeuser.com/', })) @staticmethod def with_auth(auth): class HandlerWithAuth(FakeAddHandler): auth_entity = auth
name=actor.get('displayName'), picture=actor.get('image', {}).get('url'), url=actor.get('url'), **kwargs) def silo_url(self): """Returns the Meetup account URL, e.g. https://meetup.com/members/....""" return self.gr_source.user_url(self.key.id()) def label_name(self): """Returns the username.""" return self.name class AddMeetup(oauth_meetup.CallbackHandler, util.Handler): def finish(self, auth_entity, state=None): logging.debug('finish with %s, %s', auth_entity, state) self.maybe_add_or_delete_source(Meetup, auth_entity, state) ROUTES = [ ('/meetup/start', util.oauth_starter(oauth_meetup.StartHandler).to( '/meetup/add', scopes=PUBLISH_SCOPES)), # we don't support listen ('/meetup/add', AddMeetup), ('/meetup/delete/finish', oauth_meetup.CallbackHandler.to('/delete/finish')), ('/meetup/publish/start', oauth_meetup.StartHandler.to('/meetup/publish/finish', scopes=PUBLISH_SCOPES)), ]
) ] # blogposts self.blogposts = [ BlogPost( id='https://post', source=self.sources[0].key, status='complete', feed_item={'title': 'a post'}, sent=['http://a/link'], ) ] FakeStartHandler = util.oauth_starter(OAuthStartHandler).to('/fakesource/add') class FakeAddHandler(util.Handler): """Handles the authorization callback when handling a fake source """ auth_entity = FakeAuthEntity( user_json=json_dumps({ 'id': '0123456789', 'name': 'Fake User', 'url': 'http://fakeuser.com/', })) @staticmethod def with_auth(auth): class HandlerWithAuth(FakeAddHandler):
self.response.out.write(template.render("templates/choose_blog.html", vars)) class AddTumblr(util.Handler): def post(self): auth_entity_key = util.get_required_param(self, "auth_entity_key") self.maybe_add_or_delete_source( Tumblr, ndb.Key(urlsafe=auth_entity_key).get(), util.get_required_param(self, "state"), blog_name=util.get_required_param(self, "blog"), ) class SuperfeedrNotifyHandler(superfeedr.NotifyHandler): SOURCE_CLS = Tumblr application = webapp2.WSGIApplication( [ # Tumblr doesn't seem to use scope # http://www.tumblr.com/docs/en/api/v2#oauth ("/tumblr/start", util.oauth_starter(oauth_tumblr.StartHandler).to("/tumblr/choose_blog")), ("/tumblr/choose_blog", ChooseBlog), ("/tumblr/add", AddTumblr), ("/tumblr/delete/finish", oauth_tumblr.CallbackHandler.to("/delete/finish")), ("/tumblr/notify/(.+)", SuperfeedrNotifyHandler), ], debug=appengine_config.DEBUG, )