2024-01-24 00:59:11 -08:00
|
|
|
require 'rocketamf_extensions/remote_gateway'
|
2013-01-28 13:18:11 -08:00
|
|
|
require 'ostruct'
|
2011-05-31 07:36:32 -07:00
|
|
|
|
2023-08-02 16:05:02 -07:00
|
|
|
class Pet < ApplicationRecord
|
2023-10-12 18:14:20 -07:00
|
|
|
NEOPETS_URL_ORIGIN = ENV['NEOPETS_URL_ORIGIN'] || 'https://www.neopets.com'
|
Use secret NEOPETS_URL_ORIGIN to bypass HTTPS
Oops, neopets.com finally stopped accepting `http://` connections, so our AMFPHP requests stopped working! And our current dependencies make it hard to make modern HTTPS requests :(
Instead, we're doing this quick-fix: we have a connection who knows the internal address for the Neopets origin server behind their CDN, which *does* still accept `http://` requests!
So, when `NEOPETS_URL_ORIGIN` is specified in the secret `.env` file (not committed to the repository), we'll use it instead of `http://www.neopets.com`. However, we still have that in the code as a fallback, just to be a bit less surprising to some theoretical future dev so they can see the real error message, and to self-document a bit of what that value is semantically doing! (The documentation angle is more of why it's there, rather than an actual expectation that any actual person in the future will run the code and get the fallback.)
2022-08-02 20:46:47 -07:00
|
|
|
GATEWAY_URL = NEOPETS_URL_ORIGIN + '/amfphp/gateway.php'
|
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.)
2024-04-06 02:56:40 -07:00
|
|
|
GATEWAY = RocketAMFExtensions::RemoteGateway.new(GATEWAY_URL)
|
|
|
|
CUSTOM_PET_SERVICE = GATEWAY.service('CustomPetService')
|
|
|
|
PET_SERVICE = GATEWAY.service('PetService')
|
2011-05-21 19:32:01 -07:00
|
|
|
|
2010-10-07 07:46:23 -07:00
|
|
|
belongs_to :pet_type
|
2011-05-21 19:32:01 -07:00
|
|
|
|
2024-01-24 03:25:23 -08:00
|
|
|
attr_reader :items, :pet_state, :alt_style
|
2011-05-21 19:32:01 -07:00
|
|
|
|
2023-07-22 14:04:01 -07:00
|
|
|
scope :with_pet_type_color_ids, ->(color_ids) {
|
2010-11-27 15:41:06 -08:00
|
|
|
joins(:pet_type).where(PetType.arel_table[:id].in(color_ids))
|
|
|
|
}
|
2011-05-21 19:32:01 -07:00
|
|
|
|
2024-04-06 02:31:24 -07:00
|
|
|
def load!(timeout: nil)
|
|
|
|
viewer_data = self.class.fetch_viewer_data(name, timeout:)
|
|
|
|
use_viewer_data(viewer_data)
|
2010-10-07 07:46:23 -07:00
|
|
|
end
|
2013-12-08 20:59:36 -08:00
|
|
|
|
2024-04-06 02:31:24 -07:00
|
|
|
def use_viewer_data(viewer_data)
|
2013-12-08 20:59:36 -08:00
|
|
|
pet_data = viewer_data[:custom_pet]
|
|
|
|
|
2024-01-24 03:25:23 -08:00
|
|
|
raise UnexpectedDataFormat unless pet_data[:species_id]
|
|
|
|
raise UnexpectedDataFormat unless pet_data[:color_id]
|
|
|
|
raise UnexpectedDataFormat unless pet_data[:body_id]
|
2024-01-24 00:54:30 -08:00
|
|
|
|
2024-01-24 06:12:35 -08:00
|
|
|
has_alt_style = pet_data[:alt_style].present?
|
|
|
|
|
2023-10-12 18:05:01 -07:00
|
|
|
self.pet_type = PetType.find_or_initialize_by(
|
|
|
|
species_id: pet_data[:species_id].to_i,
|
|
|
|
color_id: pet_data[:color_id].to_i
|
2013-12-08 20:59:36 -08:00
|
|
|
)
|
2024-04-06 02:25:22 -07:00
|
|
|
|
|
|
|
new_image_hash = Pet.fetch_image_hash(self.name)
|
|
|
|
self.pet_type.image_hash = new_image_hash if new_image_hash.present?
|
2024-01-24 03:25:23 -08:00
|
|
|
|
2024-01-24 06:12:35 -08:00
|
|
|
# With an alt style, `body_id` in the biology data refers to the body ID of
|
|
|
|
# the *alt* style, not the usual pet type. (We have `original_biology` for
|
|
|
|
# *some* of the pet type's situation, but not it's body ID!)
|
|
|
|
#
|
|
|
|
# So, in the alt style case, don't update `body_id` - but if this is our
|
|
|
|
# first time seeing this pet type and it doesn't *have* a `body_id` yet,
|
|
|
|
# let's not be creating it without one. We'll need to model it without the
|
|
|
|
# alt style first. (I don't bother with a clear error message though 😅)
|
|
|
|
self.pet_type.body_id = pet_data[:body_id] unless has_alt_style
|
|
|
|
if self.pet_type.body_id.nil?
|
|
|
|
raise UnexpectedDataFormat,
|
|
|
|
"can't process alt style on first occurrence of pet type"
|
|
|
|
end
|
|
|
|
|
|
|
|
pet_state_biology = has_alt_style ? pet_data[:original_biology] :
|
|
|
|
pet_data[:biology_by_zone]
|
2024-01-24 03:25:23 -08:00
|
|
|
raise UnexpectedDataFormat if pet_state_biology.empty?
|
|
|
|
pet_state_biology[0] = nil # remove effects if present
|
|
|
|
@pet_state = self.pet_type.add_pet_state_from_biology! pet_state_biology
|
|
|
|
|
2024-01-24 06:12:35 -08:00
|
|
|
if has_alt_style
|
2024-01-24 03:25:23 -08:00
|
|
|
raise UnexpectedDataFormat unless pet_data[:alt_color]
|
|
|
|
raise UnexpectedDataFormat if pet_data[:biology_by_zone].empty?
|
|
|
|
|
2024-01-24 04:01:34 -08:00
|
|
|
@alt_style = AltStyle.find_or_initialize_by(id: pet_data[:alt_style].to_i)
|
|
|
|
@alt_style.assign_attributes(
|
2024-01-24 03:25:23 -08:00
|
|
|
color_id: pet_data[:alt_color].to_i,
|
|
|
|
species_id: pet_data[:species_id].to_i,
|
|
|
|
body_id: pet_data[:body_id].to_i,
|
|
|
|
biology: pet_data[:biology_by_zone],
|
|
|
|
)
|
|
|
|
end
|
|
|
|
|
2013-12-08 20:59:36 -08:00
|
|
|
@items = Item.collection_from_pet_type_and_registries(self.pet_type,
|
2024-04-06 02:31:24 -07:00
|
|
|
viewer_data[:object_info_registry], viewer_data[:object_asset_registry])
|
2013-12-08 20:59:36 -08:00
|
|
|
end
|
2011-05-21 19:32:01 -07:00
|
|
|
|
2010-10-10 19:18:42 -07:00
|
|
|
def wardrobe_query
|
|
|
|
{
|
Oops, fix bug with saving outfits of pets loaded from Neopets.com
Okay right, the wardrobe-2020 app treats `state` as a bit of an
override thing, and `pose` is the main canonical field for how a pet
looks. We were missing a few pieces here:
1. After loading a pet, we weren't including the `pose` field in the
initial query string for the wardrobe URL, but we _were_ including
the `state` field, so the outfit would get set up with a conflicting
pet state ID vs pose.
2. When saving an outfit, we weren't taking the `state` field into
account at all. This could cause the saved outfit to not quite match
how it actually looked in-app, because the default pet state for
that species/color/pose trio could be different; and regardless, the
outfit state would come back with `appearanceId` set to `null`,
which wouldn't match the local outfit state, which would trigger an
infinite loop.
Here, we complete the round-trip of the `state` field, from pet loading
to outfit saving to the outfit data that comes back after saving!
2024-02-08 09:51:31 -08:00
|
|
|
name: self.name,
|
2024-02-16 23:22:41 -08:00
|
|
|
color: self.pet_type.color_id,
|
|
|
|
species: self.pet_type.species_id,
|
Oops, fix bug with saving outfits of pets loaded from Neopets.com
Okay right, the wardrobe-2020 app treats `state` as a bit of an
override thing, and `pose` is the main canonical field for how a pet
looks. We were missing a few pieces here:
1. After loading a pet, we weren't including the `pose` field in the
initial query string for the wardrobe URL, but we _were_ including
the `state` field, so the outfit would get set up with a conflicting
pet state ID vs pose.
2. When saving an outfit, we weren't taking the `state` field into
account at all. This could cause the saved outfit to not quite match
how it actually looked in-app, because the default pet state for
that species/color/pose trio could be different; and regardless, the
outfit state would come back with `appearanceId` set to `null`,
which wouldn't match the local outfit state, which would trigger an
infinite loop.
Here, we complete the round-trip of the `state` field, from pet loading
to outfit saving to the outfit data that comes back after saving!
2024-02-08 09:51:31 -08:00
|
|
|
pose: self.pet_state.pose,
|
|
|
|
state: self.pet_state.id,
|
|
|
|
objects: self.items.map(&:id),
|
2010-10-10 19:18:42 -07:00
|
|
|
}.to_query
|
2010-10-10 11:33:54 -07:00
|
|
|
end
|
2011-05-21 19:32:01 -07:00
|
|
|
|
2010-11-06 16:07:15 -07:00
|
|
|
def contributables
|
2024-01-24 03:54:43 -08:00
|
|
|
contributables = [pet_type, @pet_state, @alt_style].filter(&:present?)
|
2010-11-06 16:07:15 -07:00
|
|
|
items.each do |item|
|
|
|
|
contributables << item
|
|
|
|
contributables += item.pending_swf_assets
|
|
|
|
end
|
|
|
|
contributables
|
|
|
|
end
|
2011-05-21 19:32:01 -07:00
|
|
|
|
2010-11-06 15:08:42 -07:00
|
|
|
before_validation do
|
2010-11-06 16:07:15 -07:00
|
|
|
pet_type.save!
|
2011-10-31 14:22:24 -07:00
|
|
|
if @pet_state
|
|
|
|
@pet_state.save!
|
2012-01-13 13:56:31 -08:00
|
|
|
@pet_state.handle_assets!
|
2011-10-31 14:22:24 -07:00
|
|
|
end
|
|
|
|
|
2011-05-21 19:32:01 -07:00
|
|
|
if @items
|
|
|
|
@items.each do |item|
|
2013-01-28 00:10:25 -08:00
|
|
|
item.save! if item.changed?
|
2012-01-13 13:56:31 -08:00
|
|
|
item.handle_assets!
|
2011-05-21 19:32:01 -07:00
|
|
|
end
|
2010-11-06 15:08:42 -07:00
|
|
|
end
|
2024-01-24 03:25:23 -08:00
|
|
|
|
|
|
|
if @alt_style
|
|
|
|
@alt_style.save!
|
|
|
|
end
|
2010-11-06 15:08:42 -07:00
|
|
|
end
|
2011-05-21 19:32:01 -07:00
|
|
|
|
2024-04-06 02:31:24 -07:00
|
|
|
def self.load(name, **options)
|
2023-10-12 18:05:01 -07:00
|
|
|
pet = Pet.find_or_initialize_by(name: name)
|
2024-04-06 02:31:24 -07:00
|
|
|
pet.load!(**options)
|
2010-10-07 07:46:23 -07:00
|
|
|
pet
|
|
|
|
end
|
2011-05-21 19:32:01 -07:00
|
|
|
|
2024-01-24 00:51:20 -08:00
|
|
|
# NOTE: Ideally pet requests shouldn't take this long, but Neopets can be
|
|
|
|
# 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.
|
2024-04-06 02:31:24 -07:00
|
|
|
def self.fetch_viewer_data(name, timeout: 10)
|
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.)
2024-04-06 02:56:40 -07:00
|
|
|
request = CUSTOM_PET_SERVICE.action('getViewerData').request([name])
|
|
|
|
send_amfphp_request(request).tap do |data|
|
2024-04-06 02:41:28 -07:00
|
|
|
if data[:custom_pet][:name].blank?
|
|
|
|
raise PetNotFound, "Pet #{name.inspect} does not exist"
|
|
|
|
end
|
|
|
|
end
|
2024-01-24 00:51:20 -08:00
|
|
|
end
|
|
|
|
|
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.)
2024-04-06 02:56:40 -07:00
|
|
|
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"
|
2024-04-06 02:25:22 -07:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
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.)
2024-04-06 02:56:40 -07:00
|
|
|
# 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.)
|
|
|
|
def self.fetch_image_hash(name, timeout: 10)
|
|
|
|
metadata = fetch_metadata(name, timeout:)
|
|
|
|
metadata[:hash]
|
2024-04-06 02:25:22 -07:00
|
|
|
end
|
|
|
|
|
2024-01-24 00:54:30 -08:00
|
|
|
class PetNotFound < RuntimeError;end
|
|
|
|
class DownloadError < RuntimeError;end
|
2024-01-24 03:25:23 -08:00
|
|
|
class UnexpectedDataFormat < RuntimeError;end
|
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.)
2024-04-06 02:56:40 -07:00
|
|
|
|
|
|
|
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
|
2010-10-07 07:46:23 -07:00
|
|
|
end
|
2011-05-21 19:32:01 -07:00
|
|
|
|