Compare commits

...

5 commits

Author SHA1 Message Date
abfe1e6df7 Extract neopets_origin into a config value 2024-10-18 18:00:48 -07:00
e36e273d50 Extract Neopets::CustomPets service from the Pet class
Just getting this stuff out of Pet, in part because I want to start
being able to unit test modeling, and that will require stubbing out
what this service returns!
2024-10-18 17:40:31 -07:00
83e5ad6bcc Update alt styles copy to adjust for them not all being Nostalgic now 2024-10-18 17:29:48 -07:00
acb52cb870 Move NCMall and NeoPass services into a Neopets module
Just a bit more clarity of grouping! I'm also thinking about extracting
modeling APIs into a service file like this too, in which case I think
this would help clarify what it is.
2024-10-18 17:27:15 -07:00
7ef689d658 Remove unused ostruct import
Only noticed it cuz there's a deprecation warning, and so I was like,
do we use it? I think not anymore!
2024-10-18 17:20:02 -07:00
10 changed files with 85 additions and 79 deletions

View file

@ -161,7 +161,7 @@ class AuthUser < AuthRecord
# means we can wrap it in a `with_timeout` block!)
neopets_username = Sync do |task|
task.with_timeout(5) do
NeoPass.load_main_neopets_username(auth.credentials.token)
Neopets::NeoPass.load_main_neopets_username(auth.credentials.token)
end
rescue Async::TimeoutError
nil # If the request times out, just move on!

View file

@ -1,13 +1,4 @@
require 'rocketamf_extensions/remote_gateway'
require 'ostruct'
class Pet < ApplicationRecord
NEOPETS_URL_ORIGIN = ENV['NEOPETS_URL_ORIGIN'] || 'https://www.neopets.com'
GATEWAY_URL = NEOPETS_URL_ORIGIN + '/amfphp/gateway.php'
GATEWAY = RocketAMFExtensions::RemoteGateway.new(GATEWAY_URL)
CUSTOM_PET_SERVICE = GATEWAY.service('CustomPetService')
PET_SERVICE = GATEWAY.service('PetService')
belongs_to :pet_type
attr_reader :items, :pet_state, :alt_style
@ -17,7 +8,7 @@ class Pet < ApplicationRecord
}
def load!(timeout: nil)
viewer_data = self.class.fetch_viewer_data(name, timeout:)
viewer_data = Neopets::CustomPets.fetch_viewer_data(name, timeout:)
use_viewer_data(viewer_data)
end
@ -36,7 +27,7 @@ class Pet < ApplicationRecord
)
begin
new_image_hash = Pet.fetch_image_hash(self.name)
new_image_hash = Neopets::CustomPets.fetch_image_hash(self.name)
rescue => error
Rails.logger.warn "Failed to load image hash: #{error.full_message}"
end
@ -123,61 +114,5 @@ class Pet < ApplicationRecord
pet.load!(**options)
pet
end
# 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.
def self.fetch_viewer_data(name, timeout: 10)
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)
# If this is an image hash "pet name", it has no metadata.
return nil if name.start_with?("@")
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.)
def self.fetch_image_hash(name, timeout: 10)
# If this is an image hash "pet name", just take off the `@`!
return name[1..] if name.start_with?("@")
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_data = request.post(timeout: timeout, headers: {
"User-Agent" => Rails.configuration.user_agent_for_neopets,
})
rescue RocketAMFExtensions::RemoteGateway::AMFError => e
raise DownloadError, e.message
rescue RocketAMFExtensions::RemoteGateway::ConnectionError => e
raise DownloadError, e.message, e.backtrace
end
HashWithIndifferentAccess.new(response_data)
end
end

View file

@ -0,0 +1,64 @@
require 'rocketamf_extensions/remote_gateway'
module Neopets::CustomPets
GATEWAY_URL =
Addressable::URI.parse(Rails.configuration.neopets_origin) +
'/amfphp/gateway.php'
GATEWAY = RocketAMFExtensions::RemoteGateway.new(GATEWAY_URL)
CUSTOM_PET_SERVICE = GATEWAY.service('CustomPetService')
PET_SERVICE = GATEWAY.service('PetService')
class << self
# 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.
def fetch_viewer_data(name, timeout: 10)
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 fetch_metadata(name, timeout: 10)
# If this is an image hash "pet name", it has no metadata.
return nil if name.start_with?("@")
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.)
def fetch_image_hash(name, timeout: 10)
# If this is an image hash "pet name", just take off the `@`!
return name[1..] if name.start_with?("@")
metadata = fetch_metadata(name, timeout:)
metadata[:hash]
end
private
# Send an AMFPHP request, re-raising errors as `Pet::DownloadError`.
# Return the response body as a `HashWithIndifferentAccess`.
def send_amfphp_request(request, timeout: 10)
begin
response_data = request.post(timeout: timeout, headers: {
"User-Agent" => Rails.configuration.user_agent_for_neopets,
})
rescue RocketAMFExtensions::RemoteGateway::AMFError => e
raise DownloadError, e.message
rescue RocketAMFExtensions::RemoteGateway::ConnectionError => e
raise DownloadError, e.message, e.backtrace
end
HashWithIndifferentAccess.new(response_data)
end
end
end

View file

@ -1,7 +1,7 @@
require "addressable/template"
require "async/http/internet/instance"
module NCMall
module Neopets::NCMall
# Share a pool of persistent connections, rather than reconnecting on
# each request. (This library does that automatically!)
INTERNET = Async::HTTP::Internet.instance

View file

@ -2,7 +2,7 @@ require "async/http/internet/instance"
# While most of our NeoPass logic is built into Devise -> OmniAuth -> OIDC
# OmniAuth plugin, NeoPass also offers some supplemental APIs that we use here.
module NeoPass
module Neopets::NeoPass
# Share a pool of persistent connections, rather than reconnecting on
# each request. (This library does that automatically!)
INTERNET = Async::HTTP::Internet.instance

View file

@ -7,8 +7,9 @@
:markdown
Pet Styles drastically change the appearance of your pet! They're [available
in the NC Mall][1], or via "NC Trading". They're generally reminiscent of
classic Neopets designs from long ago.
in the NC Mall][1], or via "NC Trading". Some of them are "Nostalgic",
meaning they're reminiscent of classic Neopets designs from long ago—and some
are brand new!
Pet Styles only fit pets of the same species—but the *color* of the pet
doesn't matter! A Blue Acara can wear the "Nostalgic Faerie Acara" Pet Style.

View file

@ -72,6 +72,12 @@ module OpenneoImpressItems
# version number, etc. So let's only send this to Neopets systems, where it
# should hopefully be clear who we are from context!
config.user_agent_for_neopets = "Dress to Impress"
# Use the usual Neopets.com, unless we have an override. (At times, we've
# used this in collaboration with TNT to address the server directly,
# instead of through the CDN.)
config.neopets_origin =
ENV.fetch('NEOPETS_URL_ORIGIN', 'https://www.neopets.com')
end
end

View file

@ -19,10 +19,10 @@ ActiveSupport::Inflector.inflections(:en) do |inflect|
# Teach Zeitwerk that `RocketAMF` is what to expect in `lib/rocketamf`.
inflect.acronym "RocketAMF"
# Teach Zeitwerk that `NeoPass` is what to expect in `app/services/neopass.rb`.
# Teach Zeitwerk that `NeoPass` is what to expect in `neopass.rb`.
inflect.acronym "NeoPass"
# Teach Zeitwerk that "NCMall" is what to expect in `app/services/nc_mall.rb`.
# Teach Zeitwerk that "NCMall" is what to expect in `nc_mall.rb`.
# (We do this by teaching it the word "NC".)
inflect.acronym "NC"
end

View file

@ -77,17 +77,17 @@ end
def load_all_nc_mall_pages
Sync do
# First, start loading the homepage.
homepage_task = Async { NCMall.load_home_page }
homepage_task = Async { Neopets::NCMall.load_home_page }
# Next, load the page links for different categories etc.
links = NCMall.load_page_links
links = Neopets::NCMall.load_page_links
# Next, load the linked pages, 10 at a time.
barrier = Async::Barrier.new
semaphore = Async::Semaphore.new(10, parent: barrier)
begin
linked_page_tasks = links.map do |link|
semaphore.async { NCMall.load_page link[:type], link[:cat] }
semaphore.async { Neopets::NCMall.load_page link[:type], link[:cat] }
end
barrier.wait # Load all the pages.
ensure

View file

@ -1,7 +1,7 @@
namespace :pets do
desc "Load a pet's viewer data"
task :load, [:name] => [:environment] do |task, args|
pp Pet.fetch_viewer_data(args[:name])
pp Neopets::CustomPets.fetch_viewer_data(args[:name])
end
desc "Find pets that were, last we saw, of the given color and species"
@ -10,7 +10,7 @@ namespace :pets do
pt = PetType.matching_name(args.color_name, args.species_name).first!
rescue ActiveRecord::RecordNotFound
abort "Could not find pet type for " +
"#{args.color_name} #{args.species_name}"
"#{args.color_name} #{args.species_name}"
end
limit = ENV.fetch("LIMIT", 10)