# frozen_string_literal: true require 'json' require 'nokogiri' require_relative 'curlable' # Design decison to not use a class as only 'state' is in 2 env vars module Spn2 extend Curlable # for system_status include Curlable ERROR_CODES = [502].freeze class Spn2Error < StandardError; end class Spn2ErrorBadAuth < Spn2Error; end class Spn2ErrorBadAuth < Spn2Error; end class Spn2ErrorBadResponse < Spn2Error; end class Spn2ErrorInvalidOption < Spn2Error; end class Spn2ErrorUnknownResponseCode < Spn2Error; end ERROR_CODES.each { |i| Spn2.const_set("Spn2Error#{i}", Class.new(Spn2Error)) } BAD_AUTH_MSG = 'You need to be logged in to use Save Page Now.' ESSENTIAL_STATUS_KEYS = %w[job_id resources status].freeze JOB_ID_REGEXP = /^(spn2-([a-f]|\d){40})$/ WEB_ARCHIVE = 'https://web.archive.org' BINARY_OPTS = %w[capture_all capture_outlinks capture_screenshot delay_wb_availabilty force_get skip_first_archive outlinks_availability email_result].freeze OTHER_OPTS = %w[if_not_archived_within js_behavior_timeout capture_cookie target_username target_password].freeze class << self def error_classes Spn2.constants.map { |e| Spn2.const_get(e) }.select { |e| e.is_a?(Class) && e < Exception } end def access_key ENV.fetch('SPN2_ACCESS_KEY', nil) end def secret_key ENV.fetch('SPN2_SECRET_KEY', nil) end def system_status json get(url: "#{WEB_ARCHIVE}/save/status/system") # no auth end def save(url:, opts: {}) raise Spn2ErrorInvalidOption, "One or more invalid options: #{opts}" unless options_valid?(opts) hash = json(auth_post(url: "#{WEB_ARCHIVE}/save/#{url}", params: { url: url }.merge(opts))) raise Spn2ErrorBadAuth, hash.inspect if hash['message']&.== BAD_AUTH_MSG raise Spn2ErrorBadResponse, "Bad response: #{hash.inspect}" unless hash['job_id'] hash end alias capture save def status(job_id:) hash = json(auth_get(url: "#{WEB_ARCHIVE}/save/status/#{job_id}")) raise Spn2ErrorBadAuth, hash.inspect if hash['message']&.== BAD_AUTH_MSG raise Spn2ErrorBadResponse, "Bad response: #{hash.inspect}" unless (ESSENTIAL_STATUS_KEYS - hash.keys).empty? hash end private def auth_get(url:) get(url: url, headers: accept_header.merge(auth_header)) end def auth_post(url:, params: {}) post(url: url, headers: accept_header.merge(auth_header), params: params) end def accept_header { Accept: 'application/json' } end def auth_header { Authorization: "LOW #{Spn2.access_key}:#{Spn2.secret_key}" } end def json(html_string) JSON.parse(doc = Nokogiri::HTML(html_string)) rescue JSON::ParserError # an html response raise Spn2ErrorBadResponse, "No title in: #{html_string}" unless (title = doc.title) parse_error_code_from_page_title(title) end def parse_error_code_from_page_title(string) code = string.to_i raise Spn2.const_get("Spn2Error#{code}") if ERROR_CODES.include? code raise Spn2ErrorUnknownResponseCode, string end def options_valid?(opts) opts.keys.all? { |k| (BINARY_OPTS + OTHER_OPTS).include? k.to_s } end end end