lib/jekyll-twitter-plugin.rb in jekyll-twitter-plugin-1.4.0 vs lib/jekyll-twitter-plugin.rb in jekyll-twitter-plugin-2.0.0
- old
+ new
@@ -1,25 +1,28 @@
# frozen_string_literal: true
require "fileutils"
-require "twitter"
+require "net/http"
+require "uri"
+require "ostruct"
+require "json"
+require "digest"
##
# A Liquid tag plugin for Jekyll that renders Tweets from Twitter API.
# https://github.com/rob-murray/jekyll-twitter-plugin
#
module TwitterJekyll
- MissingApiKeyError = Class.new(StandardError)
- TwitterSecrets = Struct.new(:consumer_key, :consumer_secret, :access_token, :access_token_secret) do
- def self.build(source, keys)
- new(*source.values_at(*keys))
- end
- end
- CONTEXT_API_KEYS = %w(consumer_key consumer_secret access_token access_token_secret).freeze
- ENV_API_KEYS = %w(TWITTER_CONSUMER_KEY TWITTER_CONSUMER_SECRET TWITTER_ACCESS_TOKEN TWITTER_ACCESS_TOKEN_SECRET).freeze
- TWITTER_STATUS_URL = %r{\Ahttps?://twitter\.com/(:#!\/)?\w+/status(es)?/\d+}i
- REFER_TO_README = "Please see 'https://github.com/rob-murray/jekyll-twitter-plugin' for usage."
+ # TODO: remove after deprecation cycle
+ CONTEXT_API_KEYS = %w(consumer_key consumer_secret access_token access_token_secret).freeze
+ ENV_API_KEYS = %w(TWITTER_CONSUMER_KEY TWITTER_CONSUMER_SECRET TWITTER_ACCESS_TOKEN TWITTER_ACCESS_TOKEN_SECRET).freeze
+ REFER_TO_README = "Please see 'https://github.com/rob-murray/jekyll-twitter-plugin' for usage.".freeze
+ LIBRARY_VERSION = "jekyll-twitter-plugin-v2.0.0".freeze
+ REQUEST_HEADERS = { "User-Agent" => LIBRARY_VERSION }.freeze
+ # Cache class that writes to filesystem
+ # TODO: Do i really need to cache?
+ # @api private
class FileCache
def initialize(path)
@cache_folder = File.expand_path path
FileUtils.mkdir_p @cache_folder
end
@@ -47,215 +50,232 @@
def cache_filename(cache_key)
"#{cache_key}.cache"
end
end
+ # Cache class that does nothing
+ # @api private
class NullCache
def initialize(*_args); end
def read(_key); end
def write(_key, _data); end
end
- module Cacheable
- def cache_key
- Digest::MD5.hexdigest("#{self.class.name}-#{key}")
- end
+ # Wrapper around an API
+ # @api private
+ class ApiClient
+ # Perform API request; return hash with html content
+ def fetch(api_request)
+ uri = api_request.to_uri
+ response = Net::HTTP.start(uri.host, use_ssl: api_request.ssl?) do |http|
+ http.read_timeout = 5
+ http.open_timeout = 5
+ http.get uri.request_uri, REQUEST_HEADERS
+ end
- def key; end
- end
+ handle_response(api_request, response)
- class TwitterApi
- ERRORS_TO_IGNORE = [Twitter::Error::NotFound, Twitter::Error::Forbidden].freeze
-
- attr_reader :error
-
- def initialize(client, params)
- @client = client
- @status_url = params.shift
- parse_args(params)
+ rescue Timeout::Error => e
+ ErrorResponse.new(api_request, e.class.name).to_h
end
- def fetch; end
-
private
- def id_from_status_url(url)
- Regexp.last_match[1] if url.to_s =~ %r{([^\/]+$)}
+ def handle_response(api_request, response)
+ case response
+ when Net::HTTPSuccess
+ JSON.parse(response.body)
+ else
+ ErrorResponse.new(api_request, response.message).to_h
+ end
end
+ end
- def find_tweet(id)
- return unless id
-
- @client.status(id.to_i)
- rescue *ERRORS_TO_IGNORE => e
- @error = create_error(e)
- return nil
+ # @api private
+ ErrorResponse = Struct.new(:request, :message) do
+ def html
+ "<p>There was a '#{message}' error fetching URL: '#{request.entity_url}'</p>"
end
- def parse_args(args)
- @params ||= begin
- args.each_with_object({}) do |arg, params|
- k, v = arg.split("=").map(&:strip)
- if k && v
- v = Regexp.last_match[1] if v =~ /^'(.*)'$/
- params[k] = v
- end
- end
- end
+ def to_h
+ { html: html }
end
-
- def create_error(exception)
- ErrorResponse.new("There was a '#{exception.class.name}' error fetching Tweet '#{@status_url}'")
- end
end
- class Oembed < TwitterApi
- include TwitterJekyll::Cacheable
+ # Holds the URI were going to request with any parameters
+ # @api private
+ ApiRequest = Struct.new(:entity_url, :params) do
+ TWITTER_API_URL = "https://publish.twitter.com/oembed".freeze
- def fetch
- tweet_id = id_from_status_url(@status_url)
+ # Always;
+ def ssl?
+ true
+ end
- if tweet = find_tweet(tweet_id)
- # To work around a 'bug' in the Twitter gem modifying our hash we pass in
- # a copy otherwise our cache key is altered.
- @client.oembed tweet, @params.dup
- else
- error
+ # Return a URI for Twitter API with query params
+ def to_uri
+ URI.parse(TWITTER_API_URL).tap do |uri|
+ uri.query = URI.encode_www_form url_params
end
end
- private
-
- def key
- format("%s-%s", @status_url, @params.to_s)
+ # A cache key applicable to the current request with params
+ def cache_key
+ Digest::MD5.hexdigest("#{self.class.name}-#{unique_request_key}")
end
- end
- class ErrorResponse
- attr_reader :error
+ private
- def initialize(error)
- @error = error
+ def url_params
+ params.merge(url: entity_url)
end
- def html
- "<p>#{@error}</p>"
+ def unique_request_key
+ format("%s-%s", entity_url, params.to_s)
end
-
- def to_h
- { html: html }
- end
end
+ # Class to respond to Jekyll tag; entry point to library
+ # @api public
class TwitterTag < Liquid::Tag
- ERROR_BODY_TEXT = "<p>Tweet could not be processed</p>"
- DEFAULT_API_TYPE = "oembed"
+ ERROR_BODY_TEXT = "<p>Tweet could not be processed</p>".freeze
+ OEMBED_ARG = "oembed".freeze
attr_writer :cache # for testing
def initialize(_name, params, _tokens)
super
- @api_type, @params = parse_params(params)
+ @api_request = parse_params(params)
end
+ # Class that implements caching strategy
+ # @api private
def self.cache_klass
FileCache
end
+ # Return html string for Jekyll engine
+ # @api public
def render(context)
- secrets = find_secrets!(context)
- create_twitter_rest_client(secrets)
- api_client = create_api_client(@api_type, @params)
- response = cached_response(api_client) || live_response(api_client)
+ api_secrets_deprecation_warning(context) # TODO: remove after deprecation cycle
+ response = cached_response || live_response
html_output_for(response)
end
private
def cache
@cache ||= self.class.cache_klass.new("./.tweet-cache")
end
+ def api_client
+ @api_client ||= ApiClient.new
+ end
+
+ # Return Twitter response or error html
+ # @api private
def html_output_for(response)
body = (response.html if response) || ERROR_BODY_TEXT
- "<div class='embed twitter'>#{body}</div>"
+ "<div class='jekyll-twitter-plugin'>#{body}</div>"
end
- def live_response(api_client)
- if response = api_client.fetch
- cache.write(api_client.cache_key, response)
- response
+ # Return response from API and write to cache
+ # @api private
+ def live_response
+ if response = api_client.fetch(@api_request)
+ cache.write(@api_request.cache_key, response)
+ build_response(response)
end
end
- def cached_response(api_client)
- response = cache.read(api_client.cache_key)
- OpenStruct.new(response) unless response.nil?
+ # Return response cache if present, otherwise nil
+ # @api private
+ def cached_response
+ response = cache.read(@api_request.cache_key)
+ build_response(response) unless response.nil?
end
+ # Return an `ApiRequest` with the url and arguments
+ # @api private
def parse_params(params)
args = params.split(/\s+/).map(&:strip)
+ invalid_args!(args) unless args.any?
- case args[0]
- when DEFAULT_API_TYPE
- api_type, *api_args = args
- [api_type, api_args]
- when TWITTER_STATUS_URL
- [DEFAULT_API_TYPE, args]
- else
- invalid_args!(args)
+ if args[0].to_s == OEMBED_ARG # TODO: remove after deprecation cycle
+ arguments_deprecation_warning(args)
+ args.shift
end
- end
- def create_api_client(api_type, params)
- klass_name = api_type.capitalize
- api_client_klass = TwitterJekyll.const_get(klass_name)
- api_client_klass.new(@twitter_client, params)
+ url, *api_args = args
+ ApiRequest.new(url, parse_args(api_args))
end
- def create_twitter_rest_client(secrets)
- @twitter_client = Twitter::REST::Client.new do |config|
- config.consumer_key = secrets.consumer_key
- config.consumer_secret = secrets.consumer_secret
- config.access_token = secrets.access_token
- config.access_token_secret = secrets.access_token_secret
+ # Transform 'a=b x=y' tag arguments into { "a" => "b", "x" => "y" }
+ # @api private
+ def parse_args(args)
+ args.each_with_object({}) do |arg, params|
+ k, v = arg.split("=").map(&:strip)
+ if k && v
+ v = Regexp.last_match[1] if v =~ /^'(.*)'$/
+ params[k] = v
+ end
end
end
- def find_secrets!(context)
- extract_twitter_secrets_from_context(context) || extract_twitter_secrets_from_env || missing_keys!
+ # Format a response hash
+ # @api private
+ def build_response(h)
+ OpenStruct.new(h)
end
- def extract_twitter_secrets_from_context(context)
+ # TODO: remove after deprecation cycle
+ def arguments_deprecation_warning(args)
+ warn "#{LIBRARY_VERSION}: Passing '#{OEMBED_ARG}' as the first argument is not required anymore. This will result in an error in future versions.\nCalled with #{args.inspect}"
+ end
+
+ # TODO: remove after deprecation cycle
+ def api_secrets_deprecation_warning(context)
+ warn_if_twitter_secrets_in_context(context) || warn_if_twitter_secrets_in_env
+ end
+
+ # TODO: remove after deprecation cycle
+ def warn_if_twitter_secrets_in_context(context)
twitter_secrets = context.registers[:site].config.fetch("twitter", {})
return unless store_has_keys?(twitter_secrets, CONTEXT_API_KEYS)
- TwitterSecrets.build(twitter_secrets, CONTEXT_API_KEYS)
+ warn_secrets_in_project("Jekyll _config.yml")
end
- def extract_twitter_secrets_from_env
+ # TODO: remove after deprecation cycle
+ def warn_if_twitter_secrets_in_env
return unless store_has_keys?(ENV, ENV_API_KEYS)
- TwitterSecrets.build(ENV, ENV_API_KEYS)
+ warn_secrets_in_project("ENV")
end
+ # TODO: remove after deprecation cycle
+ def warn_secrets_in_project(source)
+ warn "#{LIBRARY_VERSION}: Found Twitter API keys in #{source}, this library does not require these keys anymore. You can remove these keys, if used for another library then ignore this message."
+ end
+
+ # TODO: remove after deprecation cycle
def store_has_keys?(store, keys)
keys.all? { |required_key| store.key?(required_key) }
end
- def missing_keys!
- raise MissingApiKeyError, "Twitter API keys not found. You can specify these in Jekyll config or ENV. #{REFER_TO_README}"
- end
-
+ # Raise error for invalid arguments
+ # @api private
def invalid_args!(arguments)
formatted_args = Array(arguments).join(" ")
raise ArgumentError, "Invalid arguments '#{formatted_args}' passed to 'jekyll-twitter-plugin'. #{REFER_TO_README}"
end
end
+ # Specialization of TwitterTag without any caching
+ # @api public
class TwitterTagNoCache < TwitterTag
def self.cache_klass
NullCache
end
end