From bd4b67316c9b374e64ce336288ed572421d2f80b Mon Sep 17 00:00:00 2001 From: Emi Matchu Date: Sat, 6 Apr 2024 02:56:40 -0700 Subject: [PATCH] Refactor image hash loading to use `PetService.getPet`, not CPN redirs Previously, the way we loaded the image hash for a given pet was to navigate to `https://pets.neopets.com/cpn//1/1.png`, but *not* follow the redirect, and extract the image hash from the URL where it redirected us to. In this change, we refactor to use the AMFPHP RPC `PetService.getPet` instead. I don't think it had this data last time I looked at it, but now it does! Much prefer to use an actual RPC than our weird hacky thing! (We might also be able to use this call for other stuff, like auto-labeling gender & mood for pet states, maybe?? That's in this data too! We used to load petlookups for this, long long ago, before the petlookup captchas got added.) --- app/models/pet.rb | 83 +++++++++++++++++++---------------------------- 1 file changed, 33 insertions(+), 50 deletions(-) diff --git a/app/models/pet.rb b/app/models/pet.rb index f3b73fde..1f74fb9c 100644 --- a/app/models/pet.rb +++ b/app/models/pet.rb @@ -4,8 +4,9 @@ require 'ostruct' class Pet < ApplicationRecord NEOPETS_URL_ORIGIN = ENV['NEOPETS_URL_ORIGIN'] || 'https://www.neopets.com' GATEWAY_URL = NEOPETS_URL_ORIGIN + '/amfphp/gateway.php' - PET_VIEWER = RocketAMFExtensions::RemoteGateway.new(GATEWAY_URL). - service('CustomPetService').action('getViewerData') + GATEWAY = RocketAMFExtensions::RemoteGateway.new(GATEWAY_URL) + CUSTOM_PET_SERVICE = GATEWAY.service('CustomPetService') + PET_SERVICE = GATEWAY.service('PetService') belongs_to :pet_type @@ -123,66 +124,48 @@ class Pet < ApplicationRecord # slow sometimes! Since we're on the Falcon server, long timeouts shouldn't # slow down the rest of the request queue, like it used to be in the past. def self.fetch_viewer_data(name, timeout: 10) - begin - envelope = PET_VIEWER.request([name, 0]).post(timeout: timeout) - rescue RocketAMFExtensions::RemoteGateway::AMFError => e - raise DownloadError, e.message - rescue RocketAMFExtensions::RemoteGateway::ConnectionError => e - raise DownloadError, e.message, e.backtrace - end - - HashWithIndifferentAccess.new(envelope.messages[0].data.body).tap do |data| - # When the pet doesn't exist, the service returns an empty pet object. + request = CUSTOM_PET_SERVICE.action('getViewerData').request([name]) + send_amfphp_request(request).tap do |data| if data[:custom_pet][:name].blank? raise PetNotFound, "Pet #{name.inspect} does not exist" end end end + def self.fetch_metadata(name, timeout: 10) + request = PET_SERVICE.action('getPet').request([name]) + send_amfphp_request(request).tap do |data| + if data[:name].blank? + raise PetNotFound, "Pet #{name.inspect} does not exist" + end + end + end + # Given a pet's name, load its image hash, for use in `pets.neopets.com` # image URLs. (This corresponds to its current biology and items.) - IMAGE_CPN_FORMAT = 'https://pets.neopets.com/cpn/%s/1/1.png'; - IMAGE_CP_LOCATION_REGEX = %r{^/cp/(.+?)/[0-9]+/[0-9]+\.png$}; - IMAGE_CPN_ACCEPTABLE_NAME = /^[A-Za-z0-9_]+$/ - def self.fetch_image_hash(name) - return nil unless name.match?(IMAGE_CPN_ACCEPTABLE_NAME) - - cpn_uri = URI.parse sprintf(IMAGE_CPN_FORMAT, CGI.escape(name)) - begin - res = Net::HTTP.get_response(cpn_uri, { - 'User-Agent' => Rails.configuration.user_agent_for_neopets - }) - rescue Exception => e - raise DownloadError, e.message - end - unless res.is_a? Net::HTTPFound - begin - res.error! - rescue Exception => e - Rails.logger.warn "Error loading CPN image at #{cpn_uri}: #{e.message}" - return - else - Rails.logger.warn "Error loading CPN image at #{cpn_uri}. Response: #{res.inspect}" - return - end - end - new_url = res['location'] - new_image_hash = get_hash_from_cp_path(new_url) - if new_image_hash - Rails.logger.info "Successfully loaded #{cpn_uri}, with image hash #{new_image_hash}" - new_image_hash - else - Rails.logger.warn "CPN image pointed to #{new_url}, which does not match CP image format" - end - end - - def self.get_hash_from_cp_path(path) - match = path.match(IMAGE_CP_LOCATION_REGEX) - match ? match[1] : nil + def self.fetch_image_hash(name, timeout: 10) + metadata = fetch_metadata(name, timeout:) + metadata[:hash] end class PetNotFound < RuntimeError;end class DownloadError < RuntimeError;end class UnexpectedDataFormat < RuntimeError;end + + private + + # Send an AMFPHP request, re-raising errors as `Pet::DownloadError`. + # Return the response body as a `HashWithIndifferentAccess`. + def self.send_amfphp_request(request, timeout: 10) + begin + response = request.post(timeout: timeout) + rescue RocketAMFExtensions::RemoteGateway::AMFError => e + raise DownloadError, e.message + rescue RocketAMFExtensions::RemoteGateway::ConnectionError => e + raise DownloadError, e.message, e.backtrace + end + + HashWithIndifferentAccess.new(response.messages[0].data.body) + end end