lib/jekyll-twitter-plugin.rb in jekyll-twitter-plugin-1.3.1 vs lib/jekyll-twitter-plugin.rb in jekyll-twitter-plugin-1.4.0

- old
+ new

@@ -1,16 +1,28 @@ -require 'twitter' +# frozen_string_literal: true +require "fileutils" +require "twitter" ## # 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." + class FileCache def initialize(path) - @cache_folder = File.expand_path path + @cache_folder = File.expand_path path FileUtils.mkdir_p @cache_folder end def read(key) file_to_read = cache_file(cache_filename(key)) @@ -19,11 +31,11 @@ def write(key, data) file_to_write = cache_file(cache_filename(key)) data_to_write = JSON.generate data.to_h - File.open(file_to_write, 'w') do |f| + File.open(file_to_write, "w") do |f| f.write(data_to_write) end end private @@ -36,10 +48,12 @@ "#{cache_key}.cache" end end class NullCache + def initialize(*_args); end + def read(_key); end def write(_key, _data); end end @@ -50,26 +64,26 @@ def key; end end class TwitterApi - ERRORS_TO_IGNORE = [Twitter::Error::NotFound, Twitter::Error::Forbidden] + 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) end + def fetch; end + private def id_from_status_url(url) - if url.to_s =~ /([^\/]+$)/ - Regexp.last_match[1] - end + Regexp.last_match[1] if url.to_s =~ %r{([^\/]+$)} end def find_tweet(id) return unless id @@ -79,21 +93,17 @@ return nil end def parse_args(args) @params ||= begin - params = {} - args.each do |arg| - k, v = arg.split('=').map(&:strip) + args.each_with_object({}) do |arg, params| + k, v = arg.split("=").map(&:strip) if k && v - if v =~ /^'(.*)'$/ - v = Regexp.last_match[1] - end + v = Regexp.last_match[1] if v =~ /^'(.*)'$/ params[k] = v end end - params end end def create_error(exception) ErrorResponse.new("There was a '#{exception.class.name}' error fetching Tweet '#{@status_url}'") @@ -116,21 +126,17 @@ end private def key - '%s-%s' % [@status_url, @params.to_s] + format("%s-%s", @status_url, @params.to_s) end end - class UnknownTypeClient - include TwitterJekyll::Cacheable - - def fetch; end - end - class ErrorResponse + attr_reader :error + def initialize(error) @error = error end def html @@ -141,92 +147,120 @@ { html: html } end end class TwitterTag < Liquid::Tag - ERROR_BODY_TEXT = '<p>Tweet could not be processed</p>' + ERROR_BODY_TEXT = "<p>Tweet could not be processed</p>" + DEFAULT_API_TYPE = "oembed" + attr_writer :cache # for testing + def initialize(_name, params, _tokens) super - @cache = FileCache.new('./.tweet-cache') - args = params.split(/\s+/).map(&:strip) - @api_type = args.shift - @params = args + @api_type, @params = parse_params(params) end + def self.cache_klass + FileCache + end + def render(context) - secrets = extract_twitter_secrets_from_context(context) || extract_twitter_secrets_from_env + 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) html_output_for(response) end private + def cache + @cache ||= self.class.cache_klass.new("./.tweet-cache") + end + def html_output_for(response) - body = ERROR_BODY_TEXT + body = (response.html if response) || ERROR_BODY_TEXT - if response - body = response.html || body - end - "<div class='embed twitter'>#{body}</div>" end def live_response(api_client) if response = api_client.fetch - @cache.write(api_client.cache_key, response) + cache.write(api_client.cache_key, response) response end end def cached_response(api_client) - response = @cache.read(api_client.cache_key) + response = cache.read(api_client.cache_key) OpenStruct.new(response) unless response.nil? end - def create_api_client(api_type, params) - klass_name = api_type.capitalize - if TwitterJekyll.const_defined?(klass_name) - api_client_klass = TwitterJekyll.const_get(klass_name) - api_client_klass.new(@twitter_client, params) + def parse_params(params) + args = params.split(/\s+/).map(&:strip) + + 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 - UnknownTypeClient.new + invalid_args!(args) 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) + 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 end end - TwitterSecrets = Struct.new(:consumer_key, :consumer_secret, :access_token, :access_token_secret) + def find_secrets!(context) + extract_twitter_secrets_from_context(context) || extract_twitter_secrets_from_env || missing_keys! + end def extract_twitter_secrets_from_context(context) - TwitterSecrets.new(context.registers[:site].config['twitter']['consumer_key'], context.registers[:site].config['twitter']['consumer_secret'], context.registers[:site].config['twitter']['access_token'], context.registers[:site].config['twitter']['access_token_secret']) if context_has_twitter_secrets?(context) - end + twitter_secrets = context.registers[:site].config.fetch("twitter", {}) + return unless store_has_keys?(twitter_secrets, CONTEXT_API_KEYS) - def context_has_twitter_secrets?(context) - twitter_secrets = context.registers[:site].config['twitter'] || {} - ['consumer_key', 'consumer_secret', 'access_token', 'access_token_secret'].all? {|s| twitter_secrets.key?(s)} + TwitterSecrets.build(twitter_secrets, CONTEXT_API_KEYS) end def extract_twitter_secrets_from_env - TwitterSecrets.new(ENV.fetch('TWITTER_CONSUMER_KEY'), ENV.fetch('TWITTER_CONSUMER_SECRET'), ENV.fetch('TWITTER_ACCESS_TOKEN'), ENV.fetch('TWITTER_ACCESS_TOKEN_SECRET')) + return unless store_has_keys?(ENV, ENV_API_KEYS) + + TwitterSecrets.build(ENV, ENV_API_KEYS) end + + 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 + + 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 class TwitterTagNoCache < TwitterTag - def initialize(_tag_name, _text, _token) - super - @cache = NullCache.new + def self.cache_klass + NullCache end end end -Liquid::Template.register_tag('twitter', TwitterJekyll::TwitterTag) -Liquid::Template.register_tag('twitternocache', TwitterJekyll::TwitterTagNoCache) +Liquid::Template.register_tag("twitter", TwitterJekyll::TwitterTag) +Liquid::Template.register_tag("twitternocache", TwitterJekyll::TwitterTagNoCache)