Exemple #1
0
 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()
Exemple #2
0
 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()
Exemple #3
0
 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()
Exemple #4
0
 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()
Exemple #5
0
    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()
Exemple #6
0
 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()
Exemple #7
0
 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()
Exemple #8
0
 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()
Exemple #9
0
 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()
Exemple #10
0
 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()
Exemple #11
0
  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
Exemple #12
0
  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
Exemple #13
0
 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()
Exemple #14
0
    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')))
Exemple #15
0
 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()
Exemple #16
0
  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)
Exemple #17
0
    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)
Exemple #18
0
    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(),
                }))
Exemple #19
0
  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()
Exemple #20
0
    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()
Exemple #21
0
  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()
Exemple #22
0
  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()
Exemple #23
0
  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(),
      }))
Exemple #24
0
    """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)
Exemple #25
0
    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)
Exemple #26
0

@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'])
Exemple #27
0
    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)
Exemple #28
0
                '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'])
Exemple #29
0
      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'])
Exemple #30
0
            # 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)
Exemple #31
0
    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)),
]
Exemple #32
0
    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)
Exemple #33
0
        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)
Exemple #34
0

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,
)
Exemple #35
0
                '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)
Exemple #36
0
    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)
Exemple #37
0
      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'))
Exemple #38
0
        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,
)
Exemple #39
0
    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)
Exemple #40
0
        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'])
Exemple #41
0
    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)
Exemple #42
0
        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)
Exemple #43
0
    """
    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)
Exemple #44
0
        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)
Exemple #45
0
    @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
Exemple #46
0
      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
Exemple #47
0
                      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)),
]
Exemple #48
0
            )
        ]

        # 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):
Exemple #49
0
        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,
)