diff --git a/Gemfile b/Gemfile index 230b5843..0cb1a31b 100644 --- a/Gemfile +++ b/Gemfile @@ -56,6 +56,7 @@ gem 'parallel', '~> 1.23' # For miscellaneous HTTP requests. gem "httparty", "~> 0.21.0" +gem "addressable", "~> 2.8" # For debugging. gem 'web-console', '~> 4.2', group: :development diff --git a/Gemfile.lock b/Gemfile.lock index dd4e66c0..0f0aa8cb 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -308,6 +308,7 @@ PLATFORMS DEPENDENCIES RocketAMF! + addressable (~> 2.8) bootsnap (~> 1.16) devise (~> 4.9, >= 4.9.2) devise-encryptable (~> 0.2.0) diff --git a/app/helpers/items_helper.rb b/app/helpers/items_helper.rb index 0a8dd638..2f1840c2 100644 --- a/app/helpers/items_helper.rb +++ b/app/helpers/items_helper.rb @@ -119,6 +119,19 @@ module ItemsHelper render(partial: 'items/item_link', locals: {item: item}) end + def nc_trade_value_updated_at_text(nc_trade_value) + return nil if nc_trade_value.updated_at.nil? + + # Render both "[X] [days] ago", and also the exact date, only including the + # year if it's not this same year. + time_ago_str = time_ago_in_words nc_trade_value.updated_at + date_str = nc_trade_value.updated_at.year != Date.today.year ? + nc_trade_value.updated_at.strftime("%b %-d") : + nc_trade_value.updated_at.strftime("%b %-d, %Y") + + "Last updated: #{date_str} (#{time_ago_str} ago)" + end + private def build_on_pet_types(species, special_color=nil, &block) diff --git a/app/models/item.rb b/app/models/item.rb index 08a3010f..016f5d7e 100644 --- a/app/models/item.rb +++ b/app/models/item.rb @@ -129,8 +129,17 @@ class Item < ApplicationRecord distinct } - def closeted? - @owned || @wanted + def nc_trade_value + return nil unless nc? + begin + OwlsValueGuide.find_by_name(name(:en)) + rescue OwlsValueGuide::NotFound => error + Rails.logger.debug("No NC trade value listed for #{name(:en)} (#{id})") + return nil + rescue OwlsValueGuide::NetworkError => error + Rails.logger.error("Couldn't load nc_trade_value: #{error.full_message}") + return nil + end end # Return an OrderedHash mapping users to the number of times they diff --git a/app/services/owls_value_guide.rb b/app/services/owls_value_guide.rb new file mode 100644 index 00000000..d541e0e0 --- /dev/null +++ b/app/services/owls_value_guide.rb @@ -0,0 +1,75 @@ +module OwlsValueGuide + include HTTParty + + ITEMDATA_URL_TEMPLATE = Addressable::Template.new( + "https://neo-owls.net/itemdata/{item_name}" + ) + + def self.find_by_name(item_name) + # Load the itemdata, pulling from the Rails cache if possible. + cache_key = "OwlsValueGuide/itemdata/#{item_name}" + data = Rails.cache.fetch(cache_key, expires_in: 15.minutes) do + load_itemdata(item_name) + end + + if data == :not_found + raise NotFound + end + + # Owls has records of some items that it explicitly marks as having no + # listed value. We don't care about that distinction, just return nil! + return nil if data['owls_value'].blank? + + Value.new(data['owls_value'], parse_last_updated(data['last_updated'])) + end + + Value = Struct.new(:value_text, :updated_at) + + class Error < StandardError;end + class NetworkError < Error;end + class NotFound < Error;end + + private + + def self.load_itemdata(item_name) + Rails.logger.info "[OwlsValueGuide] Loading value for #{item_name.inspect}" + + url = ITEMDATA_URL_TEMPLATE.expand(item_name: item_name) + begin + res = get(url) + rescue StandardError => error + raise NetworkError, "Couldn't connect to Owls: #{error.message}" + end + + if res.code == 404 + # Instead of raising immediately, return `:not_found` to save this + # result in the cache, then raise *after* we exit the cache block. That + # way, we won't make repeat requests for items we have that Owls + # doesn't. + return :not_found + end + + if res.code != 200 + raise NetworkError, "Owls returned status code #{res.code} (expected 200)" + end + + begin + res.parsed_response + rescue HTTParty::Error => error + raise NetworkError, "Owls returned unsupported data format: #{error.message}" + end + end + + def self.parse_last_updated(date_str) + return nil if date_str.blank? + + begin + Date.strptime(date_str, '%Y-%m-%d') + rescue Date::Error + Rails.logger.error( + "[OwlsValueGuide] unexpected last_updated format: #{date_str.inspect}" + ) + return nil + end + end +end diff --git a/app/views/items/show.html.haml b/app/views/items/show.html.haml index f9870b13..8dfc521b 100644 --- a/app/views/items/show.html.haml +++ b/app/views/items/show.html.haml @@ -9,6 +9,10 @@ - unless @item.rarity.blank? == #{t '.rarity'}: #{@item.rarity_index} (#{@item.rarity}) = link_to t('.resources.jn_items'), jn_items_url_for(@item) + - if @item.nc_trade_value + = link_to t('.resources.owls', value: @item.nc_trade_value.value_text), + "https://www.neopets.com/~owls", + title: nc_trade_value_updated_at_text(@item.nc_trade_value) - unless @item.nc? = link_to t('.resources.shop_wizard'), shop_wizard_url_for(@item) = link_to t('.resources.super_shop_wizard'), super_shop_wizard_url_for(@item) diff --git a/config/locales/en.yml b/config/locales/en.yml index d9402263..d85db5dc 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -302,6 +302,7 @@ en: rarity: Rarity resources: jn_items: JN Items + owls: "Owls: %{value}" shop_wizard: Shop Wizard super_shop_wizard: Super Wizard trading_post: Trades