amatriain/feedbunch

View on GitHub
FeedBunch-app/lib/url_subscriber.rb

Summary

Maintainability
A
25 mins
Test Coverage
# frozen_string_literal: true

require 'subscriptions_manager'
require 'feed_blacklister'
require 'url_normalizer'

##
# This class has methods to subscribe a user to a feed.

class UrlSubscriber

  ##
  # Enqueue a job to subscribe a user to a feed.
  # Receives as arguments the URL of the feed and the user that will be subscribed.
  #
  # Returns the SubscribeJobState instance representing the current state of the job.

  def self.enqueue_subscribe_job(url, user)
    Rails.logger.info "User #{user.id} - #{user.email} has requested to be subscribed to feed with fetch_url #{url}"

    # Check if the feed url is blacklisted
    if FeedBlacklister.blacklisted_url? url
      Rails.logger.warn "URL #{url} is blacklisted, cannot add subscription"
      job_state = user.subscribe_job_states.create fetch_url: url, state: SubscribeJobState::ERROR
      raise BlacklistedUrlError.new
    end

    # Check if the user is already subscribed to the feed
    existing_feed = Feed.url_variants_feed url
    if existing_feed.present?
      # Check if the user is already subscribed to the feed
      if user.feeds.include? existing_feed
        Rails.logger.info "User #{user.id} (#{user.email}) is already subscribed to feed #{existing_feed.id} - #{existing_feed.fetch_url}. No action necessary."
        job_state = user.subscribe_job_states.create fetch_url: url, state: SubscribeJobState::SUCCESS,
                                                     feed_id: existing_feed.id
        return job_state
      end
    end

    job_state = user.subscribe_job_states.create fetch_url: url
    Rails.logger.info "Enqueuing subscribe_user_job_state #{job_state.id} for user #{user.id} - #{user.email}"
    SubscribeUserWorker.perform_async user.id, url, job_state.id
    return job_state
  end

  ##
  # Subscribe a user to a feed. Receives as arguments the URL of the feed and the user that will be subscribed.
  #
  # First it checks if the feed is already in the database. In this case:
  #
  # - If the user is already subscribed to the feed, an AlreadySubscribedError is raised.
  # - Otherwise, the user is subscribed to the feed. The feed is not fetched (it is assumed its entries are
  # fresh enough).
  #
  # If the feed is not in the database, it checks if the feed can be fetched. If so, the feed is fetched,
  # parsed, saved in the database and the user is subscribed to it.
  #
  # If parsing the fetched response fails, it checks if the URL corresponds to an HTML page with feed autodiscovery
  # enabled. In this case the actual feed is fetched, saved in the database and the user subscribed to it.
  #
  # If the end result is that the user has a new subscription, returns the feed object.
  # If the user is already subscribed to the feed, raises an AlreadySubscribedError.
  # If the user has not been subscribed to a new feed (i.e. because the URL is not valid), returns nil.
  #
  # Note,- When searching for feeds in the database (to see if there is a feed with a matching URL, and whether the
  # user is already subscribed to it), this method is insensitive to trailing slashes, and if no URI-scheme is
  # present an "http://" scheme is assumed.
  #
  # E.g. if the user is subscribed to a feed with url "\http://xkcd.com/", the following URLs would cause an
  # AlreadySubscribedError to be raised:
  #
  # - "\http://xkcd.com/"
  # - "\http://xkcd.com"
  # - "\xkcd.com/"
  # - "\xkcd.com"

  def self.subscribe(url, user)
    Rails.logger.info "Subscribing user #{user.id} - #{user.email} to feed URL #{url}"

    # Ensure the url has a schema (defaults to http:// if none is passed)
    feed_url = UrlNormalizer.normalize_feed_url url

    # Try to subscribe the user to the feed assuming it's in the database
    feed = subscribe_known_feed user, feed_url

    # If the feed is not in the database, save it and fetch it for the first time.
    if feed.blank?
      feed = subscribe_new_feed user, feed_url
    end

    return feed
  end

  #############################
  # PRIVATE CLASS METHODS
  #############################

  ##
  # Subscribe a user to a feed already in the database.
  #
  # Receives as arguments:
  # - the user to subscribe
  # - the URL of the feed. It can match the feed.url (normally the website URL) or the feed.fetch_url
  # (the URL from which the feed is fetched).
  #
  # If the user is already subscribed to the feed, raises an AlreadySubscribedError.
  #
  # If there is no feed in the database matching the passed URL, returns nil.
  #
  # If a matching feed is found, the user is subscribed to it and the feed instance is returned.

  def self.subscribe_known_feed(user, feed_url)
    # Check if there is a feed with that URL already in the database
    known_feed = Feed.url_variants_feed feed_url
    if known_feed.present?
      # Check if the user is already subscribed to the feed
      if user.feeds.include? known_feed
        Rails.logger.info "User #{user.id} (#{user.email}) is already subscribed to feed #{known_feed.id} - #{known_feed.fetch_url}"
        raise AlreadySubscribedError.new known_feed
      end
      Rails.logger.info "Subscribing user #{user.id} (#{user.email}) to pre-existing feed #{known_feed.id} - #{known_feed.fetch_url}"
      SubscriptionsManager.add_subscription known_feed, user
      return known_feed
    else
      return nil
    end
  end
  private_class_method :subscribe_known_feed

  ##
  # Fetch and save a feed which is not yet in the database, and subscribe a user to it.
  #
  # Receives as argument:
  # - the user to subscribe
  # - the URL of the feed. It may be the URL from which the feed can be directly fetched, or the URL of
  # a website with feed autodiscovery enabled.
  #
  # If the fetch is successful, the feed and its entries are saved in the database, the user is subscribed to the feed
  # and the feed instance is returned.
  #
  # If the fetch is unsuccessful (e.g. no valid feed can be found at the URL), nothing is saved in the database and
  # nil is returned.

  def self.subscribe_new_feed(user, feed_url)
    Rails.logger.info "Feed #{feed_url} not in the database, trying to fetch it"
    feed = Feed.create! fetch_url: feed_url, title: feed_url
    begin
      fetched_feed = FeedClient.fetch feed, http_caching: false, perform_autodiscovery: true
      if fetched_feed
        if user.feeds.include? fetched_feed
          # Only subscribe user to the actually fetched feed if he's not already subscribed
          Rails.logger.info "Fetched feed #{feed_url} was already subscribed by user #{user.id} - #{user.email}"
          raise AlreadySubscribedError.new fetched_feed
        else
          Rails.logger.info "New feed #{feed_url} successfully fetched. Subscribing user #{user.id} - #{user.email}"
          SubscriptionsManager.add_subscription fetched_feed, user
        end

        return fetched_feed
      else
        Rails.logger.info "URL #{feed_url} is not a valid feed URL"
        feed&.destroy
        fetched_feed&.destroy
        return nil
      end
    rescue => e
      # If an error is raised during fetching, we don't keep the feed in the database
      feed.destroy
      raise e
    end
  end
  private_class_method :subscribe_new_feed

end