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/<pet_name>/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.)
This commit is contained in:
Emi Matchu 2024-04-06 02:56:40 -07:00
parent 1d3aac436b
commit bd4b67316c

View file

@ -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