Exemple #1
0
def discover():
    source = util.load_source()

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

    gr_source = source.gr_source
    if domain == gr_source.DOMAIN:
        post_id = gr_source.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 = f"Sorry, that doesn't look like a {gr_source.NAME} post URL."

    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, gr_source.post_id(link))
            source.updates = {'last_syndication_url': util.now_fn()}
            models.Source.put_updates(source)
        else:
            msg = f'Failed to fetch {util.pretty_link(url)} or find a {gr_source.NAME} syndication link.'

    else:
        msg = f'Please enter a URL on either your web site or {gr_source.NAME}.'

    flash(msg)
    return redirect(source.bridgy_url())
Exemple #2
0
    def finish(self, auth_entity, state=None):
        self.state = util.decode_oauth_state(state)
        if not state:
            self.error(
                'If you want to publish or preview, please approve the prompt.'
            )
            return redirect('/')

        source = ndb.Key(urlsafe=self.state['source_key']).get()
        if auth_entity is None:
            self.error(
                'If you want to publish or preview, please approve the prompt.'
            )
        elif not auth_entity.is_authority_for(source.auth_entity):
            self.error(
                f'Please log into {source.GR_CLASS.NAME} as {source.name} to publish that page.'
            )
        else:
            result = self._run()
            if result and result.content:
                flash(
                    f"Done! <a href=\"{self.entity.published.get('url')}\">Click here to view.</a>"
                )
                granary_message = self.entity.published.get('granary_message')
                if granary_message:
                    flash(granary_message)
            # otherwise error() added an error message

        return redirect(source.bridgy_url())
Exemple #3
0
    def new(auth_entity=None, blog_id=None, **kwargs):
        """Creates and returns a Blogger for the logged in user.

    Args:
      auth_entity: :class:`oauth_dropins.blogger.BloggerV2Auth`
      blog_id: which blog. optional. if not provided, uses the first available.
    """
        urls, domains = Blogger.urls_and_domains(auth_entity, blog_id=blog_id)
        if not urls or not domains:
            flash('Blogger blog not found. Please create one first!')
            return None

        if blog_id is None:
            for blog_id, hostname in zip(auth_entity.blog_ids,
                                         auth_entity.blog_hostnames):
                if domains[0] == hostname:
                    break
            else:
                assert False, "Internal error, shouldn't happen"

        return Blogger(id=blog_id,
                       auth_entity=auth_entity.key,
                       url=urls[0],
                       name=auth_entity.user_display_name(),
                       domains=domains,
                       domain_urls=urls,
                       picture=auth_entity.picture_url,
                       superfeedr_secret=util.generate_secret(),
                       **kwargs)
Exemple #4
0
def retry():
    entity = util.load_source()
    if not isinstance(entity, Webmentions):
        error(f'Unexpected key kind {entity.key.kind()}')

    source = entity.source.get()

    # 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()
    flash('Retrying. Refresh in a minute to see the results!')
    return redirect(request.values.get('redirect_to') or source.bridgy_url())
Exemple #5
0
  def create_new(cls, user_url=None, **kwargs):
    """Creates and saves a new :class:`Source` and adds a poll task for it.

    Args:
      user_url: a string, optional. if provided, supersedes other urls when
        determining the author_url
      **kwargs: passed to :meth:`new()`

    Returns: newly created :class:`Source`
    """
    source = cls.new(**kwargs)
    if source is None:
      return None

    if not source.domain_urls:  # defer to the source if it already set this
      auth_entity = kwargs.get('auth_entity')
      if auth_entity and hasattr(auth_entity, 'user_json'):
        source.domain_urls, source.domains = source.urls_and_domains(
          auth_entity, user_url)
    logger.debug(f'URLs/domains: {source.domain_urls} {source.domains}')

    # check if this source already exists
    existing = source.key.get()
    if existing:
      # merge some fields
      source.features = set(source.features + existing.features)
      source.populate(**existing.to_dict(include=(
            'created', 'last_hfeed_refetch', 'last_poll_attempt', 'last_polled',
            'last_syndication_url', 'last_webmention_sent', 'superfeedr_secret',
            'webmention_endpoint')))
      verb = 'Updated'
    else:
      verb = 'Added'

    author_urls = source.get_author_urls()
    link = ('http://indiewebify.me/send-webmentions/?url=' + author_urls[0]
            if author_urls else 'http://indiewebify.me/#send-webmentions')
    feature = source.features[0] if source.features else 'listen'
    blurb = '%s %s. %s' % (
      verb, source.label(),
      'Try previewing a post from your web site!' if feature == 'publish'
      else '<a href="%s">Try a webmention!</a>' % link if feature == 'webmention'
      else "Refresh in a minute to see what we've found!")
    logger.info(f'{blurb} {source.bridgy_url()}')

    source.verify()
    if source.verified():
      flash(blurb)

    source.put()

    if 'webmention' in source.features:
      superfeedr.subscribe(source)

    if 'listen' in source.features and source.AUTO_POLL:
      util.add_poll_task(source, now=True)
      util.add_poll_task(source)

    return source
Exemple #6
0
 def add_or_update_domain():
     domain = Domain.get_or_insert(
         util.domain_from_link(
             util.replace_test_domains_with_localhost(
                 auth_entity.key.id())))
     domain.auth = auth_entity.key
     if state not in domain.tokens:
         domain.tokens.append(state)
     domain.put()
     flash(f'Authorized you for {domain.key.id()}.')
Exemple #7
0
 def error(self,
           error,
           html=None,
           status=400,
           data=None,
           report=False,
           **kwargs):
     logging.info(f'publish: {error}')
     error = html or util.linkify(error)
     flash(f'{error}')
     if report:
         self.report_error(error, status=status)
Exemple #8
0
    def dispatch_request(self):
        token = request.form['token']

        try:
            to_url = self.redirect_url(state=token)
        except Exception as e:
            if util.is_connection_failure(e) or util.interpret_http_exception(
                    e)[0]:
                flash(f"Couldn't fetch your web site: {e}")
                return redirect('/')
            raise

        return redirect(to_url)
Exemple #9
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 #10
0
def crawl_now():
    source = None

    @ndb.transactional()
    def setup_refetch_hfeed():
        nonlocal source
        source = util.load_source()
        source.last_hfeed_refetch = models.REFETCH_HFEED_TRIGGER
        source.last_feed_syndication_url = None
        source.put()

    setup_refetch_hfeed()
    util.add_poll_task(source, now=True)
    flash("Crawling now. Refresh in a minute to see what's new!")
    return redirect(source.bridgy_url())
Exemple #11
0
  def finish(self, auth_entity, state=None):
    if auth_entity:
      if int(auth_entity.blog_id) == 0:
        flash('Please try again and choose a blog before clicking Authorize.')
        return redirect('/')

      # Check if this is a self-hosted WordPress blog
      site_info = WordPress.get_site_info(auth_entity)
      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)
Exemple #12
0
  def get_site_info(cls, auth_entity):
    """Fetches the site info from the API.

    Args:
      auth_entity: :class:`oauth_dropins.wordpress_rest.WordPressAuth`

    Returns:
      site info dict, or None if API calls are disabled for this blog
    """
    try:
      return cls.urlopen(auth_entity, API_SITE_URL % auth_entity.blog_id)
    except urllib.error.HTTPError as e:
      code, body = util.interpret_http_exception(e)
      if (code == '403' and '"API calls to this blog have been disabled."' in body):
        flash(f'You need to <a href="http://jetpack.me/support/json-api/">enable the Jetpack JSON API</a> in {util.pretty_link(auth_entity.blog_url)}\'s WordPress admin console.')
        redirect('/')
        return None
      raise
Exemple #13
0
def oauth_callback():
    """OAuth callback handler.

  Both the add and delete flows have to share this because Blogger's
  oauth-dropin doesn't yet allow multiple callback handlers. :/
  """
    auth_entity = None
    auth_entity_str_key = request.values.get('auth_entity')
    if auth_entity_str_key:
        auth_entity = ndb.Key(urlsafe=auth_entity_str_key).get()
        if not auth_entity.blog_ids or not auth_entity.blog_hostnames:
            auth_entity = None

    if not auth_entity:
        flash("Couldn't fetch your blogs. Maybe you're not a Blogger user?")

    state = request.values.get('state')
    if not state:
        state = util.construct_state_param_for_add(feature='webmention')

    if not auth_entity:
        util.maybe_add_or_delete_source(Blogger, auth_entity, state)
        return

    vars = {
        'action':
        '/blogger/add',
        'state':
        state,
        'operation':
        util.decode_oauth_state(state).get('operation'),
        'auth_entity_key':
        auth_entity.key.urlsafe().decode(),
        'blogs': [{
            'id': id,
            'title': title,
            'domain': host
        } for id, title, host in zip(auth_entity.blog_ids, auth_entity.
                                     blog_titles, auth_entity.blog_hostnames)],
    }
    logger.info(f'Rendering choose_blog.html with {vars}')
    return render_template('choose_blog.html', **vars)
Exemple #14
0
    def new(auth_entity=None, blog_name=None, **kwargs):
        """Creates and returns a :class:`Tumblr` for the logged in user.

    Args:
      auth_entity: :class:`oauth_dropins.tumblr.TumblrAuth`
      blog_name: which blog. optional. passed to urls_and_domains.
    """
        urls, domains = Tumblr.urls_and_domains(auth_entity,
                                                blog_name=blog_name)
        if not urls or not domains:
            flash('Tumblr blog not found. Please create one first!')
            return None

        id = domains[0]
        return Tumblr(id=id,
                      auth_entity=auth_entity.key,
                      domains=domains,
                      domain_urls=urls,
                      name=auth_entity.user_display_name(),
                      picture=TUMBLR_AVATAR_URL % id,
                      superfeedr_secret=util.generate_secret(),
                      **kwargs)
Exemple #15
0
def delete_start():
    source = util.load_source()
    kind = source.key.kind()
    feature = request.form['feature']
    state = util.encode_oauth_state({
        'operation': 'delete',
        'feature': feature,
        'source': source.key.urlsafe().decode(),
        'callback': request.values.get('callback'),
    })

    # Blogger don't support redirect_url() yet
    if kind == 'Blogger':
        return redirect(f'/blogger/delete/start?state={state}')

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

    try:
        return redirect(source.OAUTH_START(path).redirect_url(state=state))
    except werkzeug.exceptions.HTTPException:
        # raised by us, probably via self.error()
        raise
    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:
            flash(f'{source.GR_CLASS.NAME} API error {code}: {body}')
            return redirect(source.bridgy_url())
        else:
            raise
Exemple #16
0
def edit_websites_post():
    source = util.load_source()
    redirect_url = f'{request.path}?{urllib.parse.urlencode({"source_key": source.key.urlsafe().decode()})}'

    add = request.values.get('add')
    delete = request.values.get('delete')
    if (add and delete) or (not add and not delete):
        error('Either add or delete param (but not both) required')

    link = util.pretty_link(add or delete)

    if add:
        resolved = Source.resolve_profile_url(add)
        if resolved:
            if resolved in source.domain_urls:
                flash(f'{link} already exists.')
            else:
                source.domain_urls.append(resolved)
                domain = util.domain_from_link(resolved)
                source.domains.append(domain)
                source.put()
                flash(f'Added {link}.')
        else:
            flash(f"{link} doesn't look like your web site. Try again?")

    else:
        assert delete
        try:
            source.domain_urls.remove(delete)
        except ValueError:
            error(
                f"{delete} not found in {source.label()}'s current web sites")
        domain = util.domain_from_link(delete)
        if domain not in {
                util.domain_from_link(url)
                for url in source.domain_urls
        }:
            source.domains.remove(domain)
        source.put()
        flash(f'Removed {link}.')

    return redirect(redirect_url)
Exemple #17
0
def maybe_add_or_delete_source(source_cls, auth_entity, state, **kwargs):
    """Adds or deletes a source if auth_entity is not None.

  Used in each source's oauth-dropins :meth:`Callback.finish()` and
  :meth:`Callback.get()` methods, respectively.

  Args:
    source_cls: source class, e.g. :class:`instagram.Instagram`
    auth_entity: ouath-dropins auth entity
    state: string, OAuth callback state parameter. a JSON serialized dict
      with operation, feature, and an optional callback URL. For deletes,
      it will also include the source key
    kwargs: passed through to the source_cls constructor

  Returns:
    source entity if it was created or updated, otherwise None
  """
    state_obj = util.decode_oauth_state(state)
    operation = state_obj.get('operation', 'add')
    feature = state_obj.get('feature')
    callback = state_obj.get('callback')
    user_url = state_obj.get('user_url')

    logger.debug(
        'maybe_add_or_delete_source with operation=%s, feature=%s, callback=%s',
        operation, feature, callback)
    logins = None

    if operation == 'add':  # this is an add/update
        if not auth_entity:
            # TODO: only show if we haven't already flashed another message?
            # get_flashed_messages() caches so it's dangerous to call to check;
            # use eg session.get('_flashes', []) instead.
            # https://stackoverflow.com/a/17243946/186123
            flash("OK, you're not signed up. Hope you reconsider!")
            if callback:
                callback = util.add_query_params(callback,
                                                 {'result': 'declined'})
                logger.debug(
                    f'user declined adding source, redirect to external callback {callback}'
                )
                redirect(callback)
            else:
                redirect('/')

        logger.info(
            f'{source_cls.__class__.__name__}.create_new with {auth_entity.key}, {state}, {kwargs}'
        )
        source = source_cls.create_new(
            auth_entity=auth_entity,
            features=feature.split(',') if feature else [],
            user_url=user_url,
            **kwargs)

        if source:
            # if we're normalizing username case to lower case to make the key id, check
            # if there's and old Source with a capitalized key id, and if so, disable it
            # https://github.com/snarfed/bridgy/issues/884
            if source.USERNAME_KEY_ID and source.username != source.key_id():

                @ndb.transactional()
                def maybe_disable_original():
                    orig = source_cls.get_by_id(source.username)
                    if orig:
                        logging.info(
                            f'Disabling {orig.bridgy_url()} for lower case {source.bridgy_url()}'
                        )
                        orig.features = []
                        orig.put()

                maybe_disable_original()

            # add to login cookie
            logins = get_logins()
            logins.append(
                Login(path=source.bridgy_path(),
                      site=source.SHORT_NAME,
                      name=source.label_name()))

            if callback:
                callback = util.add_query_params(
                    callback, {
                        'result': 'success',
                        'user': source.bridgy_url(),
                        'key': source.key.urlsafe().decode(),
                    } if source else {'result': 'failure'})
                logger.debug(
                    'finished adding source, redirect to external callback %s',
                    callback)
                redirect(callback, logins=logins)
            elif not source.domains:
                redirect('/edit-websites?' + urllib.parse.urlencode({
                    'source_key':
                    source.key.urlsafe().decode(),
                }),
                         logins=logins)
            else:
                redirect(source.bridgy_url(), logins=logins)

        # no source
        redirect('/')

    else:  # this is a delete
        if auth_entity:
            # TODO: remove from logins cookie
            redirect(
                f'/delete/finish?auth_entity={auth_entity.key.urlsafe().decode()}&state={state}'
            )
        else:
            flash(
                f'If you want to disable, please approve the {source_cls.GR_CLASS.NAME} prompt.'
            )
            source_key = state_obj.get('source')
            if source_key:
                source = ndb.Key(urlsafe=source_key).get()
                if source:
                    redirect(source.bridgy_url())

            redirect('/')
Exemple #18
0
def delete_finish():
    parts = util.decode_oauth_state(request.values.get('state') or '')
    callback = parts and parts.get('callback')

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

    if not parts or 'feature' not in parts or 'source' not in parts:
        error('state query parameter must include "feature" and "source"')

    feature = parts['feature']
    if feature not in (Source.FEATURES):
        error(f'cannot delete unknown feature {feature}')

    logged_in_as = ndb.Key(urlsafe=request.args['auth_entity']).get()
    source = ndb.Key(urlsafe=parts['source']).get()

    logins = None
    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 = util.get_logins()
            login = util.Login(path=source.bridgy_path(),
                               site=source.SHORT_NAME,
                               name=source.label_name())
            if login in logins:
                logins.remove(login)

        if callback:
            callback = util.add_query_params(
                callback, {
                    'result': 'success',
                    'user': source.bridgy_url(),
                    'key': source.key.urlsafe().decode(),
                })
        else:
            nouns = {
                'webmention': 'webmentions',
                'listen': 'backfeed',
                'publish': 'publishing',
            }
            msg = f'Disabled {nouns[feature]} for {source.label()}.'
            if not source.features:
                msg += ' Sorry to see you go!'
            flash(msg)
    elif callback:
        callback = util.add_query_params(callback, {'result': 'failure'})
    else:
        flash(
            f'Please log into {source.GR_CLASS.NAME} as {source.name} to disable it here.'
        )

    url = callback if callback else source.bridgy_url(
    ) if source.features else '/'
    return redirect(url, logins=logins)
Exemple #19
0
def logout():
    """Redirect to the front page."""
    flash('Logged out.')
    return redirect('/', logins=[])
Exemple #20
0
def poll_now():
    source = util.load_source()
    util.add_poll_task(source, now=True)
    flash("Polling now. Refresh in a minute to see what's new!")
    return redirect(source.bridgy_url())