Example #1
0
  def post(self):
    key = ndb.Key(urlsafe=util.get_required_param(self, 'key'))
    module = self.OAUTH_MODULES[key.kind()]
    feature = util.get_required_param(self, 'feature')
    state = self.encode_state_parameter({
      'operation': 'delete',
      'feature': feature,
      'source': key.urlsafe(),
      'callback': self.request.get('callback'),
    })

    # Google+ and Blogger don't support redirect_url() yet
    if module is oauth_googleplus:
      return self.redirect('/googleplus/delete/start?state=%s' % state)

    if module is oauth_blogger_v2:
      return self.redirect('/blogger/delete/start?state=%s' % state)

    path = ('/instagram/callback' if module is indieauth
            else '/wordpress/add' if module is oauth_wordpress_rest
            else '/%s/delete/finish' % key.get().SHORT_NAME)
    kwargs = {}
    if module is oauth_twitter:
      kwargs['access_type'] = 'read' if feature == 'listen' else 'write'

    handler = module.StartHandler.to(path, **kwargs)(self.request, self.response)
    self.redirect(handler.redirect_url(state=state))
Example #2
0
  def post(self):
    logging.debug('Params: %s', self.request.params)

    key = util.get_required_param(self, 'source_key')
    source = ndb.Key(urlsafe=key).get()
    if not source or source.status == 'disabled' or 'listen' not in source.features:
      logging.error('Source not found or disabled. Dropping task.')
      return
    logging.info('Source: %s %s, %s', source.label(), source.key.string_id(),
                 source.bridgy_url(self))

    post_id = util.get_required_param(self, 'post_id')
    source.updates = {}

    try:
      activities = source.get_activities(
        fetch_replies=True, fetch_likes=True, fetch_shares=True,
        activity_id=post_id, user_id=source.key.id())
      if not activities:
        logging.info('Post %s not found.', post_id)
        return
      assert len(activities) == 1
      self.backfeed(source, activities={activities[0]['id']: activities[0]})
    except Exception, e:
      code, body = util.interpret_http_exception(e)
      if (code and (code in util.HTTP_RATE_LIMIT_CODES or code == '400' or
                    int(code) / 100 == 5)
            or util.is_connection_failure(e)):
        logging.error('API call failed; giving up. %s: %s\n%s', code, body, e)
        self.abort(util.ERROR_HTTP_RETURN_CODE)
      else:
        raise
Example #3
0
  def get(self):
    if self.request.get('declined'):
      self.messages.add("OK, you're still signed up.")
      self.redirect('/')
      return

    parts = util.get_required_param(self, 'state').split('-', 1)
    feature = parts[0]
    if len(parts) != 2 or feature not in (Source.FEATURES):
      self.abort(400, 'state query parameter must be [FEATURE]-[SOURCE KEY]')

    logged_in_as = util.get_required_param(self, 'auth_entity')
    source = ndb.Key(urlsafe=parts[1]).get()
    if logged_in_as == source.auth_entity.urlsafe():
      # TODO: remove credentials
      if feature in source.features:
        source.features.remove(feature)
        source.put()
      noun = 'webmentions' if feature == 'webmention' else feature + 'ing'
      self.messages.add('Disabled %s for %s. Sorry to see you go!' %
                        (noun, source.label()))
      # util.email_me(subject='Deleted Bridgy %s user: %s %s' %
      #               (feature, source.label(), source.key.string_id()),
      #               body=source.bridgy_url(self))
    else:
      self.messages.add('Please log into %s as %s to disable it here.' %
                        (source.AS_CLASS.NAME, source.name))

    self.redirect(source.bridgy_url(self))
Example #4
0
File: app.py Project: dev511/bridgy
  def get(self):
    if self.request.get('declined'):
      self.messages.add('If you want to disable, please approve the prompt.')
      self.redirect('/')
      return

    parts = self.decode_state_parameter(util.get_required_param(self, 'state'))
    if (not isinstance(parts, dict) or 'feature' not in parts
        or 'source' not in parts):
      self.abort(400, 'state query parameter must include "feature" and "source"')

    feature = parts['feature']
    if feature not in (Source.FEATURES):
      self.abort(400, 'cannot delete unknown feature %s' % feature)

    logged_in_as = util.get_required_param(self, 'auth_entity')
    source = ndb.Key(urlsafe=parts['source']).get()
    if logged_in_as == source.auth_entity.urlsafe():
      # TODO: remove credentials
      if feature in source.features:
        source.features.remove(feature)
        source.put()
      noun = 'webmentions' if feature == 'webmention' else feature + 'ing'
      self.messages.add('Disabled %s for %s. Sorry to see you go!' %
                        (noun, source.label()))
      # util.email_me(subject='Deleted Bridgy %s user: %s %s' %
      #               (feature, source.label(), source.key.string_id()),
      #               body=source.bridgy_url(self))
    else:
      self.messages.add('Please log into %s as %s to disable it here.' %
                        (source.AS_CLASS.NAME, source.name))

    self.redirect(source.bridgy_url(self))
Example #5
0
  def post(self):
    # load source
    try:
      source = ndb.Key(urlsafe=util.get_required_param(self, 'source_key')).get()
      if not source:
        self.abort(400, 'Source key not found')
    except ProtocolBufferDecodeError:
      logging.exception('Bad value for source_key')
      self.abort(400, 'Bad value for source_key')

    # validate URL, find silo post
    url = util.get_required_param(self, 'url')
    domain = util.domain_from_link(url)
    msg = 'Discovering now. Refresh in a minute to see the results!'

    if domain == source.GR_CLASS.DOMAIN:
      post_id = source.GR_CLASS.post_id(url)
      util.add_discover_task(source, post_id)
    elif util.domain_or_parent_in(domain, source.domains):
      synd_links = original_post_discovery.process_entry(source, url, {}, False, [])
      if synd_links:
        for link in synd_links:
          util.add_discover_task(source, source.GR_CLASS.post_id(link))
      else:
        msg = 'Failed to fetch %s or find a %s syndication link.' % (
          util.pretty_link(url), source.GR_CLASS.NAME)
    else:
      msg = 'Please enter a URL on either your web site or %s.' % source.GR_CLASS.NAME

    self.messages.add(msg)
    self.redirect(source.bridgy_url(self))
Example #6
0
 def test_get_required_param(self):
   handler = webapp2.RequestHandler(webapp2.Request.blank('/?a=b'), None)
   self.assertEqual('b', util.get_required_param(handler, 'a'))
   try:
     util.get_required_param(handler, 'c')
     self.fail('Expected HTTPException')
   except exc.HTTPException, e:
     self.assertEqual(400, e.status_int)
Example #7
0
 def test_get_required_param(self):
   handler = webapp2.RequestHandler(webapp2.Request.blank('/?a=b'), None)
   self.assertEqual('b', util.get_required_param(handler, 'a'))
   try:
     util.get_required_param(handler, 'c')
     self.fail('Expected HTTPException')
   except exc.HTTPException, e:
     self.assertEqual(400, e.status_int)
Example #8
0
 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"),
     )
Example #9
0
 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'),
     )
Example #10
0
    def get(self):
        """URL parameters:
      start_time: float, seconds since the epoch
      key: string that should appear in the first app log
    """
        start_time = util.get_required_param(self, 'start_time')
        if not util.is_float(start_time):
            self.abort(400,
                       "Couldn't convert start_time to float: %r" % start_time)
        start_time = float(start_time)

        key = util.get_required_param(self, 'key')
        if not util.is_base64(key):
            self.abort(400, 'key is not base64: %r' % key)
        key = urllib.unquote(key)

        # the propagate task logs the poll task's URL, which includes the source
        # entity key as a query param. exclude that with this heuristic.
        key_re = re.compile('[^=]' + key)

        self.response.headers['Content-Type'] = 'text/html; charset=utf-8'

        offset = None
        for log in logservice.fetch(start_time=start_time,
                                    end_time=start_time + 120,
                                    offset=offset,
                                    include_app_logs=True,
                                    version_ids=['2', '3', '4', '5', '6',
                                                 '7']):
            first_lines = '\n'.join([
                line.message.decode('utf-8')
                for line in log.app_logs[:min(10, len(log.app_logs))]
            ])
            if log.app_logs and key_re.search(first_lines):
                # found it! render and return
                self.response.out.write("""\
<html>
<body style="font-family: monospace; white-space: pre">
""")
                self.response.out.write(sanitize(log.combined))
                self.response.out.write('<br /><br />')
                for a in log.app_logs:
                    msg = a.message.decode('utf-8')
                    # don't sanitize poll task URLs since they have a key= query param
                    msg = linkify_datastore_keys(
                        util.linkify(
                            cgi.escape(msg if msg.startswith(
                                'Created by this poll:') else sanitize(msg))))
                    self.response.out.write(
                        '%s %s %s<br />' %
                        (datetime.datetime.utcfromtimestamp(a.time),
                         LEVELS[a.level], msg.replace('\n', '<br />')))
                self.response.out.write('</body>\n</html>')
                return

            offset = log.offset

        self.response.out.write('No log found!')
Example #11
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')))
Example #12
0
  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)
Example #13
0
  def get(self):
    parts = self.decode_state_parameter(util.get_required_param(self, 'state'))
    callback = parts and parts.get('callback')

    if self.request.get('declined'):
      if callback:
        # disable declined means no change took place
        callback = util.add_query_params(callback, {'result': 'declined'})
      else:
        self.messages.add('If you want to disable, please approve the prompt.')
      self.redirect(callback.encode('utf-8') if callback else '/')
      return

    if (not parts or 'feature' not in parts or 'source' not in parts):
      self.abort(400, 'state query parameter must include "feature" and "source"')

    feature = parts['feature']
    if feature not in (Source.FEATURES):
      self.abort(400, 'cannot delete unknown feature %s' % feature)

    logged_in_as = ndb.Key(
      urlsafe=util.get_required_param(self, 'auth_entity')).get()
    source = ndb.Key(urlsafe=parts['source']).get()

    if logged_in_as and logged_in_as.is_authority_for(source.auth_entity):
      # TODO: remove credentials
      if feature in source.features:
        source.features.remove(feature)
        source.put()
      noun = 'webmentions' if feature == 'webmention' else feature + 'ing'
      if callback:
        callback = util.add_query_params(callback, {
          'result': 'success',
          'user': source.bridgy_url(self),
          'key': source.key.urlsafe(),
        })
      else:
        self.messages.add('Disabled %s for %s. Sorry to see you go!' %
                          (noun, source.label()))
      # util.email_me(subject='Deleted Bridgy %s user: %s %s' %
      #               (feature, source.label(), source.key.string_id()),
      #               body=source.bridgy_url(self))
    else:
      if callback:
        callback = util.add_query_params(callback, {'result': 'failure'})
      else:
        self.messages.add('Please log into %s as %s to disable it here.' %
                          (source.GR_CLASS.NAME, source.name))

    self.redirect(callback.encode('utf-8') if callback
                  else source.bridgy_url(self) if source.features
                  else '/')
Example #14
0
  def get(self):
    """URL parameters:
      start_time: float, seconds since the epoch
      key: string that should appear in the first app log
    """
    start_time = util.get_required_param(self, 'start_time')
    if not util.is_float(start_time):
      self.abort(400, "Couldn't convert start_time to float: %r" % start_time)
    start_time = float(start_time)

    key = util.get_required_param(self, 'key')
    if not util.is_base64(key):
      self.abort(400, 'key is not base64: %r' % key)
    key = urllib.unquote(key)

    # the propagate task logs the poll task's URL, which includes the source
    # entity key as a query param. exclude that with this heuristic.
    key_re = re.compile('[^=]' + key)

    self.response.headers['Content-Type'] = 'text/html; charset=utf-8'

    offset = None
    for log in logservice.fetch(start_time=start_time, end_time=start_time + 120,
                                offset=offset, include_app_logs=True,
                                version_ids=['2', '3', '4', '5', '6', '7']):
      first_lines = '\n'.join([line.message.decode('utf-8') for line in
                               log.app_logs[:min(10, len(log.app_logs))]])
      if log.app_logs and key_re.search(first_lines):
        # found it! render and return
        self.response.out.write("""\
<html>
<body style="font-family: monospace; white-space: pre">
""")
        self.response.out.write(sanitize(log.combined))
        self.response.out.write('<br /><br />')
        for a in log.app_logs:
          msg = a.message.decode('utf-8')
          # don't sanitize poll task URLs since they have a key= query param
          msg = linkify_datastore_keys(util.linkify(cgi.escape(
              msg if msg.startswith('Created by this poll:') else sanitize(msg))))
          self.response.out.write('%s %s %s<br />' %
              (datetime.datetime.utcfromtimestamp(a.time), LEVELS[a.level],
               msg.replace('\n', '<br />')))
        self.response.out.write('</body>\n</html>')
        return

      offset = log.offset

    self.response.out.write('No log found!')
Example #15
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)

    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
Example #16
0
  def post(self):
    logging.debug('Params: %s', self.request.params)

    type = self.request.get('type')
    if type:
      assert type in ('event',)

    key = util.get_required_param(self, 'source_key')
    source = ndb.Key(urlsafe=key).get()
    if not source or source.status == 'disabled' or 'listen' not in source.features:
      logging.error('Source not found or disabled. Dropping task.')
      return
    logging.info('Source: %s %s, %s', source.label(), source.key.string_id(),
                 source.bridgy_url(self))

    post_id = util.get_required_param(self, 'post_id')
    source.updates = {}

    try:
      if type == 'event':
        activities = [source.gr_source.get_event(post_id)]
      else:
        activities = source.get_activities(
          fetch_replies=True, fetch_likes=True, fetch_shares=True,
          activity_id=post_id, user_id=source.key.id())

      if not activities or not activities[0]:
        logging.info('Post %s not found.', post_id)
        return
      assert len(activities) == 1, activities
      self.backfeed(source, activities={activities[0]['id']: activities[0]})

      in_reply_to = util.get_first(activities[0]['object'], 'inReplyTo')
      if in_reply_to:
        parsed = util.parse_tag_uri(in_reply_to.get('id', ''))  # TODO: fall back to url
        if parsed:
          util.add_discover_task(source, parsed[1])

    except Exception, e:
      code, body = util.interpret_http_exception(e)
      if (code and (code in util.HTTP_RATE_LIMIT_CODES or
                    code in ('400', '404') or
                    int(code) / 100 == 5)
            or util.is_connection_failure(e)):
        logging.error('API call failed; giving up. %s: %s\n%s', code, body, e)
        self.abort(util.ERROR_HTTP_RETURN_CODE)
      else:
        raise
Example #17
0
    def check_token_for_actor(self, actor):
        """Checks that the given actor is public and matches the request's token.

    Raises: :class:`HTTPException` with HTTP 400
    """
        if not actor:
            self.abort(400, f'Missing actor!')

        if not gr_source.Source.is_public(actor):
            self.abort(
                400,
                f'Your {self.gr_source().NAME} account is private. Bridgy only supports public accounts.'
            )

        token = util.get_required_param(self, 'token')
        domains = set(
            util.domain_from_link(util.replace_test_domains_with_localhost(u))
            for u in microformats2.object_urls(actor))
        domains.discard(self.source_class().GR_CLASS.DOMAIN)

        logging.info(f'Checking token against domains {domains}')
        for domain in ndb.get_multi(ndb.Key(Domain, d) for d in domains):
            if domain and token in domain.tokens:
                return

        self.abort(403,
                   f'Token {token} is not authorized for any of: {domains}')
Example #18
0
  def post(self):
    key = ndb.Key(urlsafe=util.get_required_param(self, 'key'))
    module = self.OAUTH_MODULES[key.kind()]
    state = '%s-%s' % (util.get_required_param(self, 'feature'), key.urlsafe())

    # Google+ and Blogger don't support redirect_url() yet
    if module is oauth_googleplus:
      self.redirect('/googleplus/delete/start?state=%s' % state)
    elif module is oauth_blogger_v2:
      self.redirect('/blogger/delete/start?state=%s' % state)
    else:
      path = ('/instagram/oauth_callback' if module is oauth_instagram
              else '/wordpress/add' if module is oauth_wordpress_rest
              else '/%s/delete/finish' % key.get().SHORT_NAME)
      handler = module.StartHandler.to(path)(self.request, self.response)
      self.redirect(handler.redirect_url(state=state))
Example #19
0
    def auth(self):
        """Loads the source and token and checks that they're valid.

    Expects token in the `token` query param, source in `key` or `username`.

    Raises: :class:`HTTPException` with HTTP 400 if the token or source are
      missing or invalid

    Returns: BrowserSource or None
    """
        # Load source
        source = util.load_source(self, param='key')
        if not source:
            self.abort(
                404,
                f'No account found for {self.gr_source().NAME} user {key or username}'
            )

        # Load and check token
        token = util.get_required_param(self, 'token')
        for domain in Domain.query(Domain.tokens == token):
            if domain.key.id() in source.domains:
                return source

        self.abort(
            403,
            f'Token {token} is not authorized for any of: {source.domains}')
Example #20
0
  def post(self):
    source = self.load_source()

    # validate URL, find silo post
    url = util.get_required_param(self, 'url')
    domain = util.domain_from_link(url)
    path = urlparse.urlparse(url).path
    msg = 'Discovering now. Refresh in a minute to see the results!'

    if domain == source.GR_CLASS.DOMAIN:
      post_id = source.GR_CLASS.post_id(url)
      if post_id:
        type = 'event' if path.startswith('/events/') else None
        util.add_discover_task(source, post_id, type=type)
      else:
        msg = "Sorry, that doesn't look like a %s post URL." % source.GR_CLASS.NAME

    elif util.domain_or_parent_in(domain, source.domains):
      synd_links = original_post_discovery.process_entry(source, url, {}, False, [])
      if synd_links:
        for link in synd_links:
          util.add_discover_task(source, source.GR_CLASS.post_id(link))
        source.updates = {'last_syndication_url': util.now_fn()}
        models.Source.put_updates(source)
      else:
        msg = 'Failed to fetch %s or find a %s syndication link.' % (
          util.pretty_link(url), source.GR_CLASS.NAME)

    else:
      msg = 'Please enter a URL on either your web site or %s.' % source.GR_CLASS.NAME

    self.messages.add(msg)
    self.redirect(source.bridgy_url(self))
Example #21
0
  def post(self):
    source = self.load_source(param='key')
    module = self.OAUTH_MODULES[source.key.kind()]
    feature = util.get_required_param(self, 'feature')
    state = util.encode_oauth_state({
      'operation': 'delete',
      'feature': feature,
      'source': source.key.urlsafe(),
      'callback': self.request.get('callback'),
    })

    # Blogger don't support redirect_url() yet
    if module is oauth_blogger_v2:
      return self.redirect('/blogger/delete/start?state=%s' % state)

    path = ('/instagram/callback' if module is indieauth
            else '/wordpress/add' if module is oauth_wordpress_rest
            else '/%s/delete/finish' % source.SHORT_NAME)
    kwargs = {}
    if module is oauth_twitter:
      kwargs['access_type'] = 'read' if feature == 'listen' else 'write'

    handler = module.StartHandler.to(path, **kwargs)(self.request, self.response)
    try:
      self.redirect(handler.redirect_url(state=state))
    except Exception as e:
      code, body = util.interpret_http_exception(e)
      if not code and util.is_connection_failure(e):
        code = '-'
        body = unicode(e)
      if code:
        self.messages.add('%s API error %s: %s' % (source.GR_CLASS.NAME, code, body))
        self.redirect(source.bridgy_url(self))
      else:
        raise
Example #22
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()
Example #23
0
  def post(self):
    source = self.load_source(param='key')
    kind = source.key.kind()
    feature = util.get_required_param(self, 'feature')
    state = util.encode_oauth_state({
      'operation': 'delete',
      'feature': feature,
      'source': source.key.urlsafe().decode(),
      'callback': self.request.get('callback'),
    })

    # Blogger don't support redirect_url() yet
    if kind == 'Blogger':
      return self.redirect('/blogger/delete/start?state=%s' % state)

    path = ('/reddit/callback' if kind == 'Reddit'
            else '/wordpress/add' if kind == 'WordPress'
            else '/%s/delete/finish' % source.SHORT_NAME)
    kwargs = {}
    if kind == 'Twitter':
      kwargs['access_type'] = 'read' if feature == 'listen' else 'write'

    handler = source.OAUTH_START_HANDLER.to(path, **kwargs)(self.request, self.response)
    try:
      self.redirect(handler.redirect_url(state=state))
    except Exception as e:
      code, body = util.interpret_http_exception(e)
      if not code and util.is_connection_failure(e):
        code = '-'
        body = str(e)
      if code:
        self.messages.add('%s API error %s: %s' % (source.GR_CLASS.NAME, code, body))
        self.redirect(source.bridgy_url(self))
      else:
        raise
Example #24
0
  def get_source(self):
    if self.source:
      return self.source

    self.source = ndb.Key(urlsafe=util.get_required_param(self, 'key')).get()
    if not self.source:
      self.abort(400, 'source not found')
Example #25
0
    def post(self):
        source = self.load_source()

        # validate URL, find silo post
        url = util.get_required_param(self, 'url')
        domain = util.domain_from_link(url)
        path = urllib.parse.urlparse(url).path
        msg = 'Discovering now. Refresh in a minute to see the results!'

        if domain == source.GR_CLASS.DOMAIN:
            post_id = source.GR_CLASS.post_id(url)
            if post_id:
                type = 'event' if path.startswith('/events/') else None
                util.add_discover_task(source, post_id, type=type)
            else:
                msg = "Sorry, that doesn't look like a %s post URL." % source.GR_CLASS.NAME

        elif util.domain_or_parent_in(domain, source.domains):
            synd_links = original_post_discovery.process_entry(
                source, url, {}, False, [])
            if synd_links:
                for link in synd_links:
                    util.add_discover_task(source,
                                           source.GR_CLASS.post_id(link))
                source.updates = {'last_syndication_url': util.now_fn()}
                models.Source.put_updates(source)
            else:
                msg = 'Failed to fetch %s or find a %s syndication link.' % (
                    util.pretty_link(url), source.GR_CLASS.NAME)

        else:
            msg = 'Please enter a URL on either your web site or %s.' % source.GR_CLASS.NAME

        self.messages.add(msg)
        self.redirect(source.bridgy_url(self))
Example #26
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()
Example #27
0
    def post(self):
        entity = ndb.Key(urlsafe=util.get_required_param(self, 'key')).get()
        if not entity:
            self.abort(400, 'key not found')
        elif not isinstance(entity, Webmentions):
            self.abort(400, 'Unexpected key kind %s', entity.key.kind())

        # run OPD to pick up any new SyndicatedPosts. note that we don't refetch
        # their h-feed, so if they've added a syndication URL since we last crawled,
        # retry won't make us pick it up. background in #524.
        if entity.key.kind() == 'Response':
            source = entity.source.get()
            for activity in [json.loads(a) for a in entity.activities_json]:
                originals, mentions = original_post_discovery.discover(
                    source,
                    activity,
                    fetch_hfeed=False,
                    include_redirect_sources=False)
                entity.unsent += original_post_discovery.targets_for_response(
                    json.loads(entity.response_json),
                    originals=originals,
                    mentions=mentions)

        entity.restart()
        self.messages.add('Retrying. Refresh in a minute to see the results!')
        self.redirect(
            self.request.get('redirect_to').encode('utf-8')
            or entity.source.get().bridgy_url(self))
Example #28
0
  def get_source(self):
    if self.source:
      return self.source

    self.source = ndb.Key(urlsafe=util.get_required_param(self, 'key')).get()
    if not self.source:
      self.abort(400, 'source not found')
Example #29
0
  def authorize(self):
    from_source = ndb.Key(urlsafe=util.get_required_param(self, 'source_key'))
    if from_source != self.source.key:
      self.error('Try publishing that page from <a href="%s">%s</a> instead.' %
                 (self.source.bridgy_path(), self.source.label()))
      return False

    return True
Example #30
0
 def post(self):
   try:
     self.redirect(self.redirect_url(state=util.get_required_param(self, 'token')))
   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
Example #31
0
  def authorize(self):
    from_source = ndb.Key(urlsafe=util.get_required_param(self, 'source_key'))
    if from_source != self.source.key:
      self.error('Try publishing that page from <a href="%s">%s</a> instead.' %
                 (self.source.bridgy_path(), self.source.label()))
      return False

    return True
Example #32
0
  def post(self):
    source = ndb.Key(urlsafe=util.get_required_param(self, 'key')).get()
    if not source:
      self.abort(400, 'source not found')

    util.add_poll_task(source, now=True)
    self.messages.add("Polling now. Refresh in a minute to see what's new!")
    self.redirect(source.bridgy_url(self))
Example #33
0
    def post(self):
        token = util.get_required_param(self, 'token')

        domains = [d.key.id() for d in Domain.query(Domain.tokens == token)]
        if not domains:
            self.abort(404, f'No registered domains for token {token}')

        self.output(domains)
Example #34
0
    def post(self):
        key = ndb.Key(urlsafe=util.get_required_param(self, 'key'))
        module = self.OAUTH_MODULES[key.kind()]
        feature = util.get_required_param(self, 'feature')
        state = util.encode_oauth_state({
            'operation':
            'delete',
            'feature':
            feature,
            'source':
            key.urlsafe(),
            'callback':
            self.request.get('callback'),
        })

        # Google+ and Blogger don't support redirect_url() yet
        if module is oauth_googleplus:
            return self.redirect('/googleplus/delete/start?state=%s' % state)

        if module is oauth_blogger_v2:
            return self.redirect('/blogger/delete/start?state=%s' % state)

        source = key.get()
        path = ('/instagram/callback' if module is indieauth else
                '/wordpress/add' if module is oauth_wordpress_rest else
                '/%s/delete/finish' % source.SHORT_NAME)
        kwargs = {}
        if module is oauth_twitter:
            kwargs['access_type'] = 'read' if feature == 'listen' else 'write'

        handler = module.StartHandler.to(path, **kwargs)(self.request,
                                                         self.response)
        try:
            self.redirect(handler.redirect_url(state=state))
        except Exception as e:
            code, body = util.interpret_http_exception(e)
            if not code and util.is_connection_failure(e):
                code = '-'
                body = unicode(e)
            if code:
                self.messages.add('%s API error %s: %s' %
                                  (source.GR_CLASS.NAME, code, body))
                self.redirect(source.bridgy_url(self))
            else:
                raise
Example #35
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()
Example #36
0
 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)
Example #37
0
 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)
Example #38
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)

        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
Example #39
0
    def post(self):
        logging.debug('Params: %s', self.request.params)

        type = self.request.get('type')
        if type:
            assert type in ('event', )

        source = util.load_source(self)
        if not source or source.status == 'disabled' or 'listen' not in source.features:
            logging.error('Source not found or disabled. Dropping task.')
            return
        logging.info('Source: %s %s, %s', source.label(),
                     source.key.string_id(), source.bridgy_url(self))

        post_id = util.get_required_param(self, 'post_id')
        source.updates = {}

        try:
            if type == 'event':
                activities = [source.gr_source.get_event(post_id)]
            else:
                activities = source.get_activities(fetch_replies=True,
                                                   fetch_likes=True,
                                                   fetch_shares=True,
                                                   activity_id=post_id,
                                                   user_id=source.key.id())

            if not activities or not activities[0]:
                logging.info('Post %s not found.', post_id)
                return
            assert len(activities) == 1, activities
            self.backfeed(source,
                          activities={activities[0]['id']: activities[0]})

            obj = activities[0].get('object') or activities[0]
            in_reply_to = util.get_first(obj, 'inReplyTo')
            if in_reply_to:
                parsed = util.parse_tag_uri(in_reply_to.get(
                    'id', ''))  # TODO: fall back to url
                if parsed:
                    util.add_discover_task(source, parsed[1])

        except Exception, e:
            code, body = util.interpret_http_exception(e)
            if (code and (code in source.RATE_LIMIT_HTTP_CODES
                          or code in ('400', '404') or int(code) / 100 == 5)
                    or util.is_connection_failure(e)):
                logging.error('API call failed; giving up. %s: %s\n%s', code,
                              body, e)
                self.abort(util.ERROR_HTTP_RETURN_CODE)
            else:
                raise
Example #40
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)
Example #41
0
  def post(self):
    entity = ndb.Key(urlsafe=util.get_required_param(self, 'key')).get()
    if not entity:
      self.abort(400, 'key not found')

    if entity.key.kind() == 'Response':
      util.add_propagate_task(entity)
    elif entity.key.kind() == 'BlogPost':
      util.add_propagate_blogpost_task(entity)
    else:
      self.abort(400, 'Unexpected key kind %s', entity.key.kind())

    self.messages.add('Retrying. Refresh in a minute to see the results!')
    self.redirect(entity.source.get().bridgy_url(self))
Example #42
0
    def post(self):
        logging.debug('Params: %s', list(self.request.params.items()))

        type = self.request.get('type')
        if type:
            assert type in ('event', )

        source = self.source = util.load_source(self)
        if not source or source.status == 'disabled' or 'listen' not in source.features:
            logging.error('Source not found or disabled. Dropping task.')
            return
        logging.info('Source: %s %s, %s', source.label(), source.key_id(),
                     source.bridgy_url(self))

        post_id = util.get_required_param(self, 'post_id')
        source.updates = {}

        if type == 'event':
            activities = [source.gr_source.get_event(post_id)]
        else:
            activities = source.get_activities(fetch_replies=True,
                                               fetch_likes=True,
                                               fetch_shares=True,
                                               activity_id=post_id,
                                               user_id=source.key_id())

        if not activities or not activities[0]:
            logging.info('Post %s not found.', post_id)
            return
        assert len(activities) == 1, activities
        activity = activities[0]
        activities = {activity['id']: activity}
        self.backfeed(source, responses=activities, activities=activities)

        obj = activity.get('object') or activity
        in_reply_to = util.get_first(obj, 'inReplyTo')
        if in_reply_to:
            parsed = util.parse_tag_uri(in_reply_to.get(
                'id', ''))  # TODO: fall back to url
            if parsed:
                util.add_discover_task(source, parsed[1])
Example #43
0
  def post(self):
    entity = ndb.Key(urlsafe=util.get_required_param(self, 'key')).get()
    if not entity:
      self.abort(400, 'key not found')

    # start all target URLs over
    if entity.status == 'complete':
      entity.status = 'new'

    targets = set(entity.unsent + entity.sent + entity.skipped + entity.error +
                  entity.failed)
    entity.sent = entity.skipped = entity.error = entity.failed = []

    # run OPD to pick up any new SyndicatedPosts. note that we don't refetch
    # their h-feed, so if they've added a syndication URL since we last crawled,
    # retry won't make us pick it up. background in #524.
    if entity.key.kind() == 'Response':
      source = entity.source.get()
      for activity in [json.loads(a) for a in entity.activities_json]:
        originals, mentions = original_post_discovery.discover(
          source, activity, fetch_hfeed=False, include_redirect_sources=False)
        targets |= original_post_discovery.targets_for_response(
          json.loads(entity.response_json), originals=originals, mentions=mentions)

    entity.unsent = targets
    entity.put()

    # clear any cached webmention endpoints
    memcache.delete_multi(util.webmention_endpoint_cache_key(url) for url in targets)

    if entity.key.kind() == 'Response':
      util.add_propagate_task(entity)
    elif entity.key.kind() == 'BlogPost':
      util.add_propagate_blogpost_task(entity)
    else:
      self.abort(400, 'Unexpected key kind %s', entity.key.kind())

    self.messages.add('Retrying. Refresh in a minute to see the results!')
    self.redirect(self.request.get('redirect_to').encode('utf-8') or
                  entity.source.get().bridgy_url(self))
Example #44
0
  def post(self):
    entity = ndb.Key(urlsafe=util.get_required_param(self, 'key')).get()
    if not entity:
      self.abort(400, 'key not found')

    # start all target URLs over
    if entity.status == 'complete':
      entity.status = 'new'

    targets = set(entity.unsent + entity.sent + entity.skipped + entity.error +
                  entity.failed)
    entity.sent = entity.skipped = entity.error = entity.failed = []

    # run OPD to pick up any new SyndicatedPosts. note that we don't refetch
    # their h-feed, so if they've added a syndication URL since we last crawled,
    # retry won't make us pick it up. background in #524.
    if entity.key.kind() == 'Response':
      source = entity.source.get()
      for activity in [json.loads(a) for a in entity.activities_json]:
        originals, mentions = original_post_discovery.discover(
          source, activity, fetch_hfeed=False, include_redirect_sources=False)
        targets |= original_post_discovery.targets_for_response(
          json.loads(entity.response_json), originals=originals, mentions=mentions)

    entity.unsent = targets
    entity.put()

    # clear any cached webmention endpoints
    memcache.delete_multi(util.webmention_endpoint_cache_key(url) for url in targets)

    if entity.key.kind() == 'Response':
      util.add_propagate_task(entity)
    elif entity.key.kind() == 'BlogPost':
      util.add_propagate_blogpost_task(entity)
    else:
      self.abort(400, 'Unexpected key kind %s', entity.key.kind())

    self.messages.add('Retrying. Refresh in a minute to see the results!')
    self.redirect(self.request.get('redirect_to').encode('utf-8') or
                  entity.source.get().bridgy_url(self))
Example #45
0
  def post(self):
    entity = ndb.Key(urlsafe=util.get_required_param(self, 'key')).get()
    if not entity:
      self.abort(400, 'key not found')
    elif not isinstance(entity, Webmentions):
      self.abort(400, 'Unexpected key kind %s', entity.key.kind())

    # run OPD to pick up any new SyndicatedPosts. note that we don't refetch
    # their h-feed, so if they've added a syndication URL since we last crawled,
    # retry won't make us pick it up. background in #524.
    if entity.key.kind() == 'Response':
      source = entity.source.get()
      for activity in [json.loads(a) for a in entity.activities_json]:
        originals, mentions = original_post_discovery.discover(
          source, activity, fetch_hfeed=False, include_redirect_sources=False)
        entity.unsent += original_post_discovery.targets_for_response(
          json.loads(entity.response_json), originals=originals, mentions=mentions)

    entity.restart()
    self.messages.add('Retrying. Refresh in a minute to see the results!')
    self.redirect(self.request.get('redirect_to').encode('utf-8') or
                  entity.source.get().bridgy_url(self))
Example #46
0
    def post(self, *args):
        source = self.auth()

        gr_src = self.gr_source()
        id = util.get_required_param(self, 'id')

        # validate request
        parsed_id = util.parse_tag_uri(id)
        if not parsed_id:
            self.abort(400, f'Expected id to be tag URI; got {id}')

        activity = Activity.get_by_id(id)
        if not activity:
            self.abort(404, f'No {gr_src.NAME} post found for id {id}')
        elif activity.source != source.key:
            self.abort(
                403,
                f'Activity {id} is owned by {activity.source}, not {source.key}'
            )

        activity_data = json_loads(activity.activity_json)

        # convert new reactions to AS, merge into existing activity
        try:
            new_reactions = gr_src.merge_scraped_reactions(
                self.request.text, activity_data)
        except ValueError as e:
            msg = "Couldn't parse scraped reactions: %s" % e
            logging.error(msg, stack_info=True)
            self.abort(400, msg)

        activity.activity_json = json_dumps(activity_data)
        activity.put()

        reaction_ids = ' '.join(r['id'] for r in new_reactions)
        logging.info(f"Stored reactions for activity {id}: {reaction_ids}")
        self.output(new_reactions)
Example #47
0
 def post(self):
   return self.start_oauth_flow(util.get_required_param(self, 'feature'))
Example #48
0
 def source_url(self):
   return util.get_required_param(self, 'source')
Example #49
0
 def target_url(self):
   return util.get_required_param(self, 'target')
Example #50
0
  def get(self):
    parts = self.decode_state_parameter(self.request.get('state') or '')
    callback = parts and parts.get('callback')

    if self.request.get('declined'):
      # disable declined means no change took place
      if callback:
        callback = util.add_query_params(callback, {'result': 'declined'})
        self.redirect(callback.encode('utf-8'))
      else:
        self.messages.add('If you want to disable, please approve the prompt.')
        self.redirect('/')
      return

    if (not parts or 'feature' not in parts or 'source' not in parts):
      self.abort(400, 'state query parameter must include "feature" and "source"')

    feature = parts['feature']
    if feature not in (Source.FEATURES):
      self.abort(400, 'cannot delete unknown feature %s' % feature)

    logged_in_as = ndb.Key(
      urlsafe=util.get_required_param(self, 'auth_entity')).get()
    source = ndb.Key(urlsafe=parts['source']).get()

    if logged_in_as and logged_in_as.is_authority_for(source.auth_entity):
      # TODO: remove credentials
      if feature in source.features:
        source.features.remove(feature)
        source.put()

        # remove login cookie
        logins = self.get_logins()
        login = util.Login(path=source.bridgy_path(), site=source.SHORT_NAME,
                           name=source.label_name())
        if login in logins:
          logins.remove(login)
          self.set_logins(logins)

      noun = 'webmentions' if feature == 'webmention' else feature + 'ing'
      if callback:
        callback = util.add_query_params(callback, {
          'result': 'success',
          'user': source.bridgy_url(self),
          'key': source.key.urlsafe(),
        })
      else:
        self.messages.add('Disabled %s for %s. Sorry to see you go!' %
                          (noun, source.label()))
      # util.email_me(subject='Deleted Bridgy %s user: %s %s' %
      #               (feature, source.label(), source.key.string_id()),
      #               body=source.bridgy_url(self))
    else:
      if callback:
        callback = util.add_query_params(callback, {'result': 'failure'})
      else:
        self.messages.add('Please log into %s as %s to disable it here.' %
                          (source.GR_CLASS.NAME, source.name))

    self.redirect(callback.encode('utf-8') if callback
                  else source.bridgy_url(self) if source.features
                  else '/')
Example #51
0
 def target_url(self):
     return util.get_required_param(self, 'target')
Example #52
0
 def source_url(self):
     return util.get_required_param(self, 'source')
Example #53
0
 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'))
Example #54
0
 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'))
Example #55
0
 def post(self):
   auth_entity = ndb.Key(
     urlsafe=util.get_required_param(self, 'auth_entity_key')).get()
   state = util.get_required_param(self, 'state')
   id = util.get_required_param(self, 'blog')
   self.maybe_add_or_delete_source(Medium, auth_entity, state, id=id)
Example #56
0
  def post(self, source_short_name):
    logging.info('Params: %self', self.request.params.items())
    # strip fragments from source and target url
    self.source_url = urlparse.urldefrag(util.get_required_param(self, 'source'))[0]
    self.target_url = urlparse.urldefrag(util.get_required_param(self, 'target'))[0]

    # clean target url (strip utm_* query params)
    self.target_url = util.clean_webmention_url(self.target_url)

    # parse and validate target URL
    domain = util.domain_from_link(self.target_url)
    if not domain:
      return self.error(msg, 'Could not parse target URL %s' % self.target_url)

    # look up source by domain
    source_cls = SOURCES[source_short_name]
    domain = domain.lower()
    self.source = (source_cls.query()
                   .filter(source_cls.domains == domain)
                   .filter(source_cls.features == 'webmention')
                   .filter(source_cls.status == 'enabled')
                   .get())
    if not self.source:
      return self.error(
        'Could not find %s account for %s. Is it registered with Bridgy?' %
        (source_cls.AS_CLASS.NAME, domain),
        mail=False)

    # create BlogWebmention entity
    id = '%s %s' % (self.source_url, self.target_url)
    self.entity = BlogWebmention.get_or_insert(id, source=self.source.key)
    if self.entity.status == 'complete':
      # TODO: response message saying update isn't supported
      self.response.write(self.entity.published)
      return
    logging.debug('BlogWebmention entity: %s', self.entity.key.urlsafe())

    # fetch source page
    resp = self.fetch_mf2(self.source_url)
    if not resp:
      return
    self.fetched, data = resp

    item = self.find_mention_item(data)
    if not item:
      return self.error('Could not find target URL %s in source page %s' %
                        (self.target_url, self.fetched.url),
                        data=data, log_exception=False)

    # default author to target domain
    author_name = domain
    author_url = 'http://%s/' % domain

    # extract author name and URL from h-card, if any
    props = item['properties']
    author = first_value(props, 'author')
    if author:
      if isinstance(author, basestring):
        author_name = author
      else:
        author_props = author.get('properties', {})
        author_name = first_value(author_props, 'name')
        author_url = first_value(author_props, 'url')

    # if present, u-url overrides source url
    u_url = first_value(props, 'url')
    if u_url:
      self.entity.u_url = u_url

    # generate content
    content = props['content'][0]  # find_mention_item() guaranteed this is here
    text = (content.get('html') or content.get('value')).strip()
    text += ' <br /> <a href="%s">via %s</a>' % (
      self.entity.source_url(), util.domain_from_link(self.entity.source_url()))

    # write comment
    try:
      self.entity.published = self.source.create_comment(
        self.target_url, author_name, author_url, text)
    except urllib2.HTTPError, e:
      body = e.read()
      logging.error('Error response body: %r', body)
      return self.error('Error: %s; %s' % (e, body), status=e.code)
Example #57
0
  def post(self, source_short_name):
    logging.info('Params: %self', self.request.params.items())
    # strip fragments from source and target url
    self.source_url = urlparse.urldefrag(util.get_required_param(self, 'source'))[0]
    self.target_url = urlparse.urldefrag(util.get_required_param(self, 'target'))[0]

    # follow target url through any redirects, strip utm_* query params
    resp = util.follow_redirects(self.target_url)
    redirected_target_urls = [r.url for r in resp.history]
    self.target_url = util.clean_url(resp.url)

    # parse and validate target URL
    domain = util.domain_from_link(self.target_url)
    if not domain:
      return self.error('Could not parse target URL %s' % self.target_url)

    # look up source by domain
    source_cls = models.sources[source_short_name]
    domain = domain.lower()
    self.source = (source_cls.query()
                   .filter(source_cls.domains == domain)
                   .filter(source_cls.features == 'webmention')
                   .filter(source_cls.status == 'enabled')
                   .get())
    if not self.source:
      return self.error(
        'Could not find %s account for %s. Is it registered with Bridgy?' %
        (source_cls.GR_CLASS.NAME, domain))

    if urlparse.urlparse(self.target_url).path in ('', '/'):
      return self.error('Home page webmentions are not currently supported.')

    # create BlogWebmention entity
    id = u'%s %s' % (self.source_url, self.target_url)
    self.entity = BlogWebmention.get_or_insert(
      id, source=self.source.key, redirected_target_urls=redirected_target_urls)
    if self.entity.status == 'complete':
      # TODO: response message saying update isn't supported
      self.response.write(self.entity.published)
      return
    logging.debug('BlogWebmention entity: %s', self.entity.key.urlsafe())

    # fetch source page
    resp = self.fetch_mf2(self.source_url)
    if not resp:
      return
    self.fetched, data = resp

    item = self.find_mention_item(data)
    if not item:
      return self.error('Could not find target URL %s in source page %s' %
                        (self.target_url, self.fetched.url),
                        data=data, log_exception=False)

    # default author to target domain
    author_name = domain
    author_url = 'http://%s/' % domain

    # extract author name and URL from h-card, if any
    props = item['properties']
    author = first_value(props, 'author')
    if author:
      if isinstance(author, basestring):
        author_name = author
      else:
        author_props = author.get('properties', {})
        author_name = first_value(author_props, 'name')
        author_url = first_value(author_props, 'url')

    # if present, u-url overrides source url
    u_url = first_value(props, 'url')
    if u_url:
      self.entity.u_url = u_url

    # generate content
    content = props['content'][0]  # find_mention_item() guaranteed this is here
    text = (content.get('html') or content.get('value')).strip()
    source_url = self.entity.source_url()
    text += ' <br /> <a href="%s">via %s</a>' % (
      source_url, util.domain_from_link(source_url))

    # write comment
    try:
      self.entity.published = self.source.create_comment(
        self.target_url, author_name, author_url, text)
    except Exception, e:
      code, body = util.interpret_http_exception(e)
      msg = 'Error: %s %s; %s' % (code, e, body)
      if code == '401':
        logging.warning('Disabling source!')
        self.source.status = 'disabled'
        self.source.put()
        return self.error(msg, status=code, mail=False)
      elif code == '404':
        # post is gone
        return self.error(msg, status=code, mail=False)
      elif code or body:
        return self.error(msg, status=code, mail=True)
      else:
        raise
Example #58
0
    def post(self, source_short_name):
        logging.info('Params: %s', list(self.request.params.items()))
        # strip fragments from source and target url
        self.source_url = urllib.parse.urldefrag(
            util.get_required_param(self, 'source'))[0]
        self.target_url = urllib.parse.urldefrag(
            util.get_required_param(self, 'target'))[0]

        # follow target url through any redirects, strip utm_* query params
        resp = util.follow_redirects(self.target_url)
        redirected_target_urls = [r.url for r in resp.history]
        self.target_url = util.clean_url(resp.url)

        # parse and validate target URL
        domain = util.domain_from_link(self.target_url)
        if not domain:
            return self.error('Could not parse target URL %s' %
                              self.target_url)

        # look up source by domain
        source_cls = models.sources[source_short_name]
        domain = domain.lower()
        self.source = (source_cls.query().filter(
            source_cls.domains == domain).filter(
                source_cls.features == 'webmention').filter(
                    source_cls.status == 'enabled').get())
        if not self.source:
            # check for a rel-canonical link. Blogger uses these when it serves a post
            # from multiple domains, e.g country TLDs like epeus.blogspot.co.uk vs
            # epeus.blogspot.com.
            # https://github.com/snarfed/bridgy/issues/805
            mf2 = self.fetch_mf2(self.target_url, require_mf2=False)
            if not mf2:
                # fetch_mf2() already wrote the error response
                return
            domains = util.dedupe_urls(
                util.domain_from_link(url)
                for url in mf2[1]['rels'].get('canonical', []))
            if domains:
                self.source = (source_cls.query().filter(
                    source_cls.domains.IN(domains)).filter(
                        source_cls.features == 'webmention').filter(
                            source_cls.status == 'enabled').get())

        if not self.source:
            return self.error(
                'Could not find %s account for %s. Is it registered with Bridgy?'
                % (source_cls.GR_CLASS.NAME, domain))

        # check that the target URL path is supported
        target_path = urllib.parse.urlparse(self.target_url).path
        if target_path in ('', '/'):
            return self.error(
                'Home page webmentions are not currently supported.',
                status=202)
        for pattern in self.source.PATH_BLOCKLIST:
            if pattern.match(target_path):
                return self.error(
                    '%s webmentions are not supported for URL path: %s' %
                    (self.source.GR_CLASS.NAME, target_path),
                    status=202)

        # create BlogWebmention entity
        id = '%s %s' % (self.source_url, self.target_url)
        self.entity = BlogWebmention.get_or_insert(
            id,
            source=self.source.key,
            redirected_target_urls=redirected_target_urls)
        if self.entity.status == 'complete':
            # TODO: response message saying update isn't supported
            self.response.write(self.entity.published)
            return
        logging.debug("BlogWebmention entity: '%s'",
                      self.entity.key.urlsafe().decode())

        # fetch source page
        fetched = self.fetch_mf2(self.source_url)
        if not fetched:
            return
        resp, mf2 = fetched

        item = self.find_mention_item(mf2.get('items', []))
        if not item:
            return self.error(
                'Could not find target URL %s in source page %s' %
                (self.target_url, resp.url),
                data=mf2,
                log_exception=False)

        # default author to target domain
        author_name = domain
        author_url = 'http://%s/' % domain

        # extract author name and URL from h-card, if any
        props = item['properties']
        author = first_value(props, 'author')
        if author:
            if isinstance(author, str):
                author_name = author
            else:
                author_props = author.get('properties', {})
                author_name = first_value(author_props, 'name')
                author_url = first_value(author_props, 'url')

        # if present, u-url overrides source url
        u_url = first_value(props, 'url')
        if u_url:
            self.entity.u_url = u_url

        # generate content
        content = props['content'][
            0]  # find_mention_item() guaranteed this is here
        text = (content.get('html') or content.get('value')).strip()
        source_url = self.entity.source_url()
        text += ' <br /> <a href="%s">via %s</a>' % (
            source_url, util.domain_from_link(source_url))

        # write comment
        try:
            self.entity.published = self.source.create_comment(
                self.target_url, author_name, author_url, text)
        except Exception as e:
            code, body = util.interpret_http_exception(e)
            msg = 'Error: %s %s; %s' % (code, e, body)
            if code == '401':
                logging.warning('Disabling source due to: %s' % e,
                                stack_info=True)
                self.source.status = 'disabled'
                self.source.put()
                return self.error(msg,
                                  status=code,
                                  report=self.source.is_beta_user())
            elif code == '404':
                # post is gone
                return self.error(msg, status=code, report=False)
            elif util.is_connection_failure(e) or (code
                                                   and int(code) // 100 == 5):
                return self.error(msg,
                                  status=util.ERROR_HTTP_RETURN_CODE,
                                  report=False)
            elif code or body:
                return self.error(msg, status=code, report=True)
            else:
                raise

        # write results to datastore
        self.entity.status = 'complete'
        self.entity.put()
        self.response.write(json_dumps(self.entity.published))