Compare commits

...

12 commits

Author SHA1 Message Date
e511bdc5e2 Oops right, it's okay to work with an alt style we've already seen! 2024-01-24 04:01:34 -08:00
4e5023288e Track contributions of Alt Styles 2024-01-24 03:54:43 -08:00
1933046809 Try to fix the pet load limiter
This is hard to test directly, but this is my guess from what I'm
reading in this? https://stackoverflow.com/a/32958124/107415
2024-01-24 03:26:52 -08:00
5004142dfb Add alt style support to modeling
Nothing to show them yet, but I think this works for loading it all in
the first place?

Still needs contributions tho!
2024-01-24 03:25:23 -08:00
ec3bb7dbe0 Update NC UC announcement 2024-01-24 02:15:50 -08:00
4888a9dcbe Oops, remove stray viewer_data reader from Pet
This is left over from a previous plan I had, before moving
`fetch_viewer_data` to a class method.
2024-01-24 01:00:53 -08:00
b24ed7facb Re-enable pet loading, now that we're filtering out alt styles for now 2024-01-24 01:00:25 -08:00
b0e7f2ccd5 Move lib/rocketamf -> lib/rocketamf_extensions, to fix reload issues
Something in the Rails loader doesn't like that I have both a gem and
a lib folder named `RocketAMF`, I think? It'll often work for the first
pet load request, then on subsequent ones say `Envelope` is not
defined, I'm guessing because it scrapped the gem's module in favor of
mine?

Idk, let's just simplify all this by making our own module. I feel like
this old lib could use an overhaul and simplification anyway, but this
will do for now!
2024-01-24 00:59:11 -08:00
0406a32444 Disable loading alt style pets specifically 2024-01-24 00:54:30 -08:00
1057fdd3a9 Add rake task to load pet data
Just a quick lil shortcut to look up a pet, I've wanted this recently!
2024-01-24 00:51:37 -08:00
c33f1cb767 Extract Pet.fetch_viewer_data to a class method
This will make it easy to look up pet data from Neopets without messing
around with how the Pet model handles it!
2024-01-24 00:51:20 -08:00
c76e8cd2a9 Allow admin users to load pets
Also allows the new rake task we're building to load, too!
2024-01-24 00:42:35 -08:00
17 changed files with 179 additions and 59 deletions

View file

@ -11,7 +11,8 @@ class ContributionsController < ApplicationController
@contributions, @contributions,
:scopes => { :scopes => {
'Item' => Item.includes(:translations), 'Item' => Item.includes(:translations),
'PetType' => PetType.includes(:species, :color) 'PetType' => PetType.includes(:species, :color),
'AltStyle' => AltStyle.includes(:species, :color),
} }
) )
end end

View file

@ -1,10 +1,13 @@
class PetsController < ApplicationController class PetsController < ApplicationController
rescue_from Pet::PetNotFound, :with => :pet_not_found rescue_from Pet::PetNotFound, with: :pet_not_found
rescue_from PetType::DownloadError, SwfAsset::DownloadError, :with => :asset_download_error rescue_from PetType::DownloadError, SwfAsset::DownloadError, with: :asset_download_error
rescue_from Pet::DownloadError, :with => :pet_download_error rescue_from Pet::DownloadError, with: :pet_download_error
rescue_from Pet::ModelingDisabled, with: :modeling_disabled rescue_from Pet::UnexpectedDataFormat, with: :unexpected_data_format
def load def load
# Uncomment this to temporarily disable modeling for most users.
# return modeling_disabled unless user_signed_in? && current_user.admin?
raise Pet::PetNotFound unless params[:name] raise Pet::PetNotFound unless params[:name]
@pet = Pet.load( @pet = Pet.load(
params[:name], params[:name],
@ -80,4 +83,9 @@ class PetsController < ApplicationController
pet_load_error long_message: t('pets.load.modeling_disabled'), pet_load_error long_message: t('pets.load.modeling_disabled'),
status: :forbidden status: :forbidden
end end
def unexpected_data_format
pet_load_error long_message: t('pets.load.unexpected_data_format'),
status: :internal_server_error
end
end end

View file

@ -9,6 +9,8 @@ module ContributionHelper
contributed_pet_type('pet_type', contributed, show_image) contributed_pet_type('pet_type', contributed, show_image)
when PetState when PetState
contributed_pet_type('pet_state', contributed.pet_type, show_image) contributed_pet_type('pet_state', contributed.pet_type, show_image)
when AltStyle
contributed_alt_style(contributed, show_image)
end end
end end
@ -37,6 +39,20 @@ module ContributionHelper
output output
end end
def contributed_alt_style(alt_style, show_image)
span = content_tag(:span, alt_style.name, class: 'contributed-name')
output = translate("contributions.contributed_description.main.alt_style_html",
alt_style_name: span)
# HACK: Just assume this is a Nostalgic Alt Style, and that the thumbnail
# is named reliably!
if show_image
thumbnail_url = "https://images.neopets.com/items/nostalgic_" +
"#{alt_style.color.name.downcase}_#{alt_style.species.name.downcase}.gif"
output << image_tag(thumbnail_url)
end
output
end
private private
def output(&block) def output(&block)

22
app/models/alt_style.rb Normal file
View file

@ -0,0 +1,22 @@
class AltStyle < ApplicationRecord
belongs_to :species
belongs_to :color
has_many :parent_swf_asset_relationships, as: :parent
has_many :swf_assets, through: :parent_swf_asset_relationships
has_many :contributions, as: :contributed, inverse_of: :contributed
def name
I18n.translate('pet_types.human_name', color_human_name: color.human_name,
species_human_name: species.human_name)
end
def biology=(biology)
# TODO: This is very similar to what `PetState` does, but like… much much
# more compact? Idk if I'm missing something, or if I was just that much
# more clueless back when I wrote it, lol 😅
self.swf_assets = biology.values.map do |asset_data|
SwfAsset.from_biology_data(self.body_id, asset_data)
end
end
end

View file

@ -3,7 +3,8 @@ class Contribution < ApplicationRecord
'Item' => 3, 'Item' => 3,
'SwfAsset' => 2, 'SwfAsset' => 2,
'PetType' => 15, 'PetType' => 15,
'PetState' => 10 'PetState' => 10,
'AltStyle' => 30,
} }
belongs_to :contributed, :polymorphic => true belongs_to :contributed, :polymorphic => true
@ -24,7 +25,7 @@ class Contribution < ApplicationRecord
'SwfAsset' => 'Item', 'SwfAsset' => 'Item',
'PetState' => 'PetType' 'PetState' => 'PetType'
} }
CONTRIBUTED_CHILDREN = CONTRIBUTED_RELATIONSHIPS.keys CONTRIBUTED_CHILDREN = CONTRIBUTED_RELATIONSHIPS.keys + ['AltStyle']
CONTRIBUTED_TYPES = CONTRIBUTED_CHILDREN + CONTRIBUTED_RELATIONSHIPS.values CONTRIBUTED_TYPES = CONTRIBUTED_CHILDREN + CONTRIBUTED_RELATIONSHIPS.values
def self.preload_contributeds_and_parents(contributions, options={}) def self.preload_contributeds_and_parents(contributions, options={})
options[:scopes] ||= {} options[:scopes] ||= {}

View file

@ -1,28 +1,29 @@
require 'rocketamf/remote_gateway' require 'rocketamf_extensions/remote_gateway'
require 'ostruct' require 'ostruct'
class Pet < ApplicationRecord class Pet < ApplicationRecord
NEOPETS_URL_ORIGIN = ENV['NEOPETS_URL_ORIGIN'] || 'https://www.neopets.com' NEOPETS_URL_ORIGIN = ENV['NEOPETS_URL_ORIGIN'] || 'https://www.neopets.com'
GATEWAY_URL = NEOPETS_URL_ORIGIN + '/amfphp/gateway.php' GATEWAY_URL = NEOPETS_URL_ORIGIN + '/amfphp/gateway.php'
PET_VIEWER = RocketAMF::RemoteGateway.new(GATEWAY_URL). PET_VIEWER = RocketAMFExtensions::RemoteGateway.new(GATEWAY_URL).
service('CustomPetService').action('getViewerData') service('CustomPetService').action('getViewerData')
PET_NOT_FOUND_REMOTE_ERROR = 'PHP: Unable to retrieve records from the database.' PET_NOT_FOUND_REMOTE_ERROR = 'PHP: Unable to retrieve records from the database.'
WARDROBE_PATH = '/wardrobe' WARDROBE_PATH = '/wardrobe'
belongs_to :pet_type belongs_to :pet_type
attr_reader :items, :pet_state attr_reader :items, :pet_state, :alt_style
scope :with_pet_type_color_ids, ->(color_ids) { scope :with_pet_type_color_ids, ->(color_ids) {
joins(:pet_type).where(PetType.arel_table[:id].in(color_ids)) joins(:pet_type).where(PetType.arel_table[:id].in(color_ids))
} }
class ModelingDisabled < RuntimeError;end
def load!(options={}) def load!(options={})
raise ModelingDisabled
options[:locale] ||= I18n.default_locale options[:locale] ||= I18n.default_locale
I18n.with_locale(options.delete(:locale)) do I18n.with_locale(options.delete(:locale)) do
use_viewer_data(fetch_viewer_data(options.delete(:timeout)), options) use_viewer_data(
self.class.fetch_viewer_data(name, options.delete(:timeout)),
options,
)
end end
true true
end end
@ -32,44 +33,41 @@ class Pet < ApplicationRecord
pet_data = viewer_data[:custom_pet] pet_data = viewer_data[:custom_pet]
raise UnexpectedDataFormat unless pet_data[:species_id]
raise UnexpectedDataFormat unless pet_data[:color_id]
raise UnexpectedDataFormat unless pet_data[:body_id]
self.pet_type = PetType.find_or_initialize_by( self.pet_type = PetType.find_or_initialize_by(
species_id: pet_data[:species_id].to_i, species_id: pet_data[:species_id].to_i,
color_id: pet_data[:color_id].to_i color_id: pet_data[:color_id].to_i
) )
self.pet_type.body_id = pet_data[:body_id] self.pet_type.body_id = pet_data[:body_id]
self.pet_type.origin_pet = self self.pet_type.origin_pet = self
biology = pet_data[:biology_by_zone]
biology[0] = nil # remove effects if present pet_state_biology = pet_data[:alt_style] ?
@pet_state = self.pet_type.add_pet_state_from_biology! biology pet_data[:original_biology] : pet_data[:biology_by_zone]
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
if pet_data[:alt_style]
raise UnexpectedDataFormat unless pet_data[:alt_color]
raise UnexpectedDataFormat if pet_data[:biology_by_zone].empty?
@alt_style = AltStyle.find_or_initialize_by(id: pet_data[:alt_style].to_i)
@alt_style.assign_attributes(
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
@items = Item.collection_from_pet_type_and_registries(self.pet_type, @items = Item.collection_from_pet_type_and_registries(self.pet_type,
viewer_data[:object_info_registry], viewer_data[:object_asset_registry], viewer_data[:object_info_registry], viewer_data[:object_asset_registry],
options[:item_scope]) options[:item_scope])
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 fetch_viewer_data(timeout=10, locale=nil)
locale ||= I18n.default_locale
begin
neopets_language_code = I18n.compatible_neopets_language_code_for(locale)
envelope = PET_VIEWER.request([name, 0]).post(
:timeout => timeout,
:headers => {
'Cookie' => "lang=#{neopets_language_code}"
}
)
rescue RocketAMF::RemoteGateway::AMFError => e
if e.message == PET_NOT_FOUND_REMOTE_ERROR
raise PetNotFound, "Pet #{name.inspect} does not exist"
end
raise DownloadError, e.message
rescue RocketAMF::RemoteGateway::ConnectionError => e
raise DownloadError, e.message, e.backtrace
end
HashWithIndifferentAccess.new(envelope.messages[0].data.body)
end
def wardrobe_query def wardrobe_query
{ {
:name => self.name, :name => self.name,
@ -81,7 +79,7 @@ class Pet < ApplicationRecord
end end
def contributables def contributables
contributables = [pet_type, @pet_state] contributables = [pet_type, @pet_state, @alt_style].filter(&:present?)
items.each do |item| items.each do |item|
contributables << item contributables << item
contributables += item.pending_swf_assets contributables += item.pending_swf_assets
@ -102,6 +100,10 @@ class Pet < ApplicationRecord
item.handle_assets! item.handle_assets!
end end
end end
if @alt_style
@alt_style.save!
end
end end
def self.load(name, options={}) def self.load(name, options={})
@ -116,7 +118,32 @@ class Pet < ApplicationRecord
pet pet
end end
class PetNotFound < Exception;end # NOTE: Ideally pet requests shouldn't take this long, but Neopets can be
class DownloadError < Exception;end # 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, locale=nil)
locale ||= I18n.default_locale
begin
neopets_language_code = I18n.compatible_neopets_language_code_for(locale)
envelope = PET_VIEWER.request([name, 0]).post(
:timeout => timeout,
:headers => {
'Cookie' => "lang=#{neopets_language_code}"
}
)
rescue RocketAMFExtensions::RemoteGateway::AMFError => e
if e.message == PET_NOT_FOUND_REMOTE_ERROR
raise PetNotFound, "Pet #{name.inspect} does not exist"
end
raise DownloadError, e.message
rescue RocketAMFExtensions::RemoteGateway::ConnectionError => e
raise DownloadError, e.message, e.backtrace
end
HashWithIndifferentAccess.new(envelope.messages[0].data.body)
end
class PetNotFound < RuntimeError;end
class DownloadError < RuntimeError;end
class UnexpectedDataFormat < RuntimeError;end
end end

View file

@ -180,6 +180,15 @@ class SwfAsset < ApplicationRecord
self.manifest_url = parsed_manifest_url.to_s self.manifest_url = parsed_manifest_url.to_s
end end
def self.from_biology_data(body_id, data)
remote_id = data[:part_id].to_i
swf_asset = SwfAsset.find_or_initialize_by type: 'biology',
remote_id: remote_id
swf_asset.body_id = body_id
swf_asset.origin_biology_data = data
swf_asset
end
def self.from_wardrobe_link_params(ids) def self.from_wardrobe_link_params(ids)
where(( where((
arel_table[:remote_id].in(ids[:biology]).and(arel_table[:type].eq('biology')) arel_table[:remote_id].in(ids[:biology]).and(arel_table[:type].eq('biology'))

View file

@ -2,10 +2,12 @@
= advertise_campaign_progress @campaign = advertise_campaign_progress @campaign
.warning .notice
%strong Happy NC UC day! %strong Happy NC UC day!
We've temporarily disabled pet loading while we get everything set up and We're still working on Alt Styles support, but other pets can be loaded as
investigate some new compatibility issues. We'll have it back soon! usual!
%br
Excited to have them for you soon!
%p#pet-not-found.alert= t 'pets.load.not_found' %p#pet-not-found.alert= t 'pets.load.not_found'

View file

@ -12,7 +12,7 @@ Rack::Attack.throttled_responder = lambda do |req|
if req.path.end_with?('.json') if req.path.end_with?('.json')
[503, {}, [PETS_THROTTLE_MESSAGE]] [503, {}, [PETS_THROTTLE_MESSAGE]]
else else
flash = req.flash flash = req.env['action_dispatch.request.flash_hash']
flash[:warning] = PETS_THROTTLE_MESSAGE flash[:warning] = PETS_THROTTLE_MESSAGE
[302, {"Location" => "/"}, [PETS_THROTTLE_MESSAGE]] [302, {"Location" => "/"}, [PETS_THROTTLE_MESSAGE]]
end end

View file

@ -232,6 +232,7 @@ en:
swf_asset_html: "%{item_description} on a new body type" swf_asset_html: "%{item_description} on a new body type"
pet_type_html: "%{pet_type_description} for the first time" pet_type_html: "%{pet_type_description} for the first time"
pet_state_html: "a new pose for %{pet_type_description}" pet_state_html: "a new pose for %{pet_type_description}"
alt_style_html: "a new Alt Style of the %{alt_style_name}"
contribution: contribution:
description_html: "%{user_link} showed us %{contributed_description}" description_html: "%{user_link} showed us %{contributed_description}"
@ -784,6 +785,10 @@ en:
modeling_disabled: We've turned off pet loading for a bit, while we modeling_disabled: We've turned off pet loading for a bit, while we
investigate some changes in how it works. We'll be back as soon as we investigate some changes in how it works. We'll be back as soon as we
can! can!
unexpected_data_format:
We found the pet and what it's wearing, but the data isn't in quite the
format we usually expect, so we're stopping to make sure we don't make
a mistake. Sorry about this—if it keeps happening, let us know!
swf_assets: swf_assets:
links: links:

View file

@ -0,0 +1,11 @@
class CreateAltStyles < ActiveRecord::Migration[7.1]
def change
create_table :alt_styles do |t|
t.references :species, type: :integer, null: false, foreign_key: true
t.references :color, type: :integer, null: false, foreign_key: true
t.integer :body_id, null: false
t.timestamps
end
end
end

View file

@ -10,7 +10,17 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.1].define(version: 2024_01_23_133215) do ActiveRecord::Schema[7.1].define(version: 2024_01_24_102340) do
create_table "alt_styles", charset: "utf8mb4", collation: "utf8mb4_general_ci", force: :cascade do |t|
t.integer "species_id", null: false
t.integer "color_id", null: false
t.integer "body_id", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["color_id"], name: "index_alt_styles_on_color_id"
t.index ["species_id"], name: "index_alt_styles_on_species_id"
end
create_table "auth_servers", id: :integer, charset: "latin1", collation: "latin1_swedish_ci", force: :cascade do |t| create_table "auth_servers", id: :integer, charset: "latin1", collation: "latin1_swedish_ci", force: :cascade do |t|
t.string "short_name", limit: 10, null: false t.string "short_name", limit: 10, null: false
t.string "name", limit: 40, null: false t.string "name", limit: 40, null: false
@ -301,4 +311,6 @@ ActiveRecord::Schema[7.1].define(version: 2024_01_23_133215) do
t.string "plain_label", null: false t.string "plain_label", null: false
end end
add_foreign_key "alt_styles", "colors"
add_foreign_key "alt_styles", "species"
end end

View file

@ -1,8 +1,8 @@
require 'net/http' require 'net/http'
require 'rocketamf' require 'rocketamf'
require File.join(File.dirname(__FILE__), 'remote_gateway', 'service') require_relative 'remote_gateway/service'
module RocketAMF module RocketAMFExtensions
class RemoteGateway class RemoteGateway
attr_reader :uri attr_reader :uri

View file

@ -1,6 +1,6 @@
require File.join(File.dirname(__FILE__), 'request') require_relative 'request'
module RocketAMF module RocketAMFExtensions
class RemoteGateway class RemoteGateway
class Action class Action
attr_reader :service, :name attr_reader :service, :name

View file

@ -1,6 +1,6 @@
require 'timeout' require 'timeout'
module RocketAMF module RocketAMFExtensions
class RemoteGateway class RemoteGateway
class Request class Request
ERROR_CODE = 'AMFPHP_RUNTIME_ERROR' ERROR_CODE = 'AMFPHP_RUNTIME_ERROR'
@ -51,7 +51,7 @@ module RocketAMF
first_message_data = HashWithIndifferentAccess.new(result.messages[0].data) first_message_data = HashWithIndifferentAccess.new(result.messages[0].data)
if first_message_data.respond_to?(:[]) && first_message_data[:code] == ERROR_CODE if first_message_data.respond_to?(:[]) && first_message_data[:code] == ERROR_CODE
raise AMFError.new(first_message_data) raise RocketAMF::AMFError.new(first_message_data)
end end
result result
@ -60,17 +60,17 @@ module RocketAMF
private private
def envelope def envelope
output = Envelope.new output = RocketAMF::Envelope.new
output.messages << wrapper_message output.messages << wrapper_message
output output
end end
def wrapper_message def wrapper_message
message = Message.new 'null', '/1', [remoting_message] message = RocketAMF::Message.new 'null', '/1', [remoting_message]
end end
def remoting_message def remoting_message
message = Values::RemotingMessage.new message = RocketAMF::Values::RemotingMessage.new
message.source = @action.service.name message.source = @action.service.name
message.operation = @action.name message.operation = @action.name
message.body = @params message.body = @params

View file

@ -1,6 +1,6 @@
require File.join(File.dirname(__FILE__), 'action') require_relative 'action'
module RocketAMF module RocketAMFExtensions
class RemoteGateway class RemoteGateway
class Service class Service
attr_reader :gateway, :name attr_reader :gateway, :name

6
lib/tasks/pets.rake Normal file
View file

@ -0,0 +1,6 @@
namespace :pets do
desc "Load a pet's viewer data"
task :load, [:name] => [:environment] do |task, args|
pp Pet.fetch_viewer_data(args[:name])
end
end