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!) # means we can wrap it in a `with_timeout` block!)
neopets_username = Sync do |task| neopets_username = Sync do |task|
task.with_timeout(5) do task.with_timeout(5) do
NeoPass.load_main_neopets_username(auth.credentials.token) Neopets::NeoPass.load_main_neopets_username(auth.credentials.token)
end end
rescue Async::TimeoutError rescue Async::TimeoutError
nil # If the request times out, just move on! 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 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 belongs_to :pet_type
attr_reader :items, :pet_state, :alt_style attr_reader :items, :pet_state, :alt_style
@ -17,7 +8,7 @@ class Pet < ApplicationRecord
} }
def load!(timeout: nil) 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) use_viewer_data(viewer_data)
end end
@ -36,7 +27,7 @@ class Pet < ApplicationRecord
) )
begin begin
new_image_hash = Pet.fetch_image_hash(self.name) new_image_hash = Neopets::CustomPets.fetch_image_hash(self.name)
rescue => error rescue => error
Rails.logger.warn "Failed to load image hash: #{error.full_message}" Rails.logger.warn "Failed to load image hash: #{error.full_message}"
end end
@ -123,61 +114,5 @@ class Pet < ApplicationRecord
pet.load!(**options) pet.load!(**options)
pet pet
end 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 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 "addressable/template"
require "async/http/internet/instance" require "async/http/internet/instance"
module NCMall module Neopets::NCMall
# Share a pool of persistent connections, rather than reconnecting on # Share a pool of persistent connections, rather than reconnecting on
# each request. (This library does that automatically!) # each request. (This library does that automatically!)
INTERNET = Async::HTTP::Internet.instance 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 # While most of our NeoPass logic is built into Devise -> OmniAuth -> OIDC
# OmniAuth plugin, NeoPass also offers some supplemental APIs that we use here. # 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 # Share a pool of persistent connections, rather than reconnecting on
# each request. (This library does that automatically!) # each request. (This library does that automatically!)
INTERNET = Async::HTTP::Internet.instance INTERNET = Async::HTTP::Internet.instance

View file

@ -7,8 +7,9 @@
:markdown :markdown
Pet Styles drastically change the appearance of your pet! They're [available 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 in the NC Mall][1], or via "NC Trading". Some of them are "Nostalgic",
classic Neopets designs from long ago. 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 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. 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 # version number, etc. So let's only send this to Neopets systems, where it
# should hopefully be clear who we are from context! # should hopefully be clear who we are from context!
config.user_agent_for_neopets = "Dress to Impress" 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
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`. # Teach Zeitwerk that `RocketAMF` is what to expect in `lib/rocketamf`.
inflect.acronym "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" 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".) # (We do this by teaching it the word "NC".)
inflect.acronym "NC" inflect.acronym "NC"
end end

View file

@ -77,17 +77,17 @@ end
def load_all_nc_mall_pages def load_all_nc_mall_pages
Sync do Sync do
# First, start loading the homepage. # 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. # 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. # Next, load the linked pages, 10 at a time.
barrier = Async::Barrier.new barrier = Async::Barrier.new
semaphore = Async::Semaphore.new(10, parent: barrier) semaphore = Async::Semaphore.new(10, parent: barrier)
begin begin
linked_page_tasks = links.map do |link| 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 end
barrier.wait # Load all the pages. barrier.wait # Load all the pages.
ensure ensure

View file

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