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,
:scopes => {
'Item' => Item.includes(:translations),
'PetType' => PetType.includes(:species, :color)
'PetType' => PetType.includes(:species, :color),
'AltStyle' => AltStyle.includes(:species, :color),
}
)
end

View file

@ -1,10 +1,13 @@
class PetsController < ApplicationController
rescue_from Pet::PetNotFound, :with => :pet_not_found
rescue_from PetType::DownloadError, SwfAsset::DownloadError, :with => :asset_download_error
rescue_from Pet::DownloadError, :with => :pet_download_error
rescue_from Pet::ModelingDisabled, with: :modeling_disabled
rescue_from Pet::PetNotFound, with: :pet_not_found
rescue_from PetType::DownloadError, SwfAsset::DownloadError, with: :asset_download_error
rescue_from Pet::DownloadError, with: :pet_download_error
rescue_from Pet::UnexpectedDataFormat, with: :unexpected_data_format
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]
@pet = Pet.load(
params[:name],
@ -80,4 +83,9 @@ class PetsController < ApplicationController
pet_load_error long_message: t('pets.load.modeling_disabled'),
status: :forbidden
end
def unexpected_data_format
pet_load_error long_message: t('pets.load.unexpected_data_format'),
status: :internal_server_error
end
end

View file

@ -9,6 +9,8 @@ module ContributionHelper
contributed_pet_type('pet_type', contributed, show_image)
when PetState
contributed_pet_type('pet_state', contributed.pet_type, show_image)
when AltStyle
contributed_alt_style(contributed, show_image)
end
end
@ -36,6 +38,20 @@ module ContributionHelper
output << image_tag(sprintf(PET_TYPE_IMAGE_FORMAT, pet_type.image_hash)) if show_image
output
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

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

View file

@ -1,28 +1,29 @@
require 'rocketamf/remote_gateway'
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'
PET_VIEWER = RocketAMF::RemoteGateway.new(GATEWAY_URL).
PET_VIEWER = RocketAMFExtensions::RemoteGateway.new(GATEWAY_URL).
service('CustomPetService').action('getViewerData')
PET_NOT_FOUND_REMOTE_ERROR = 'PHP: Unable to retrieve records from the database.'
WARDROBE_PATH = '/wardrobe'
belongs_to :pet_type
attr_reader :items, :pet_state
attr_reader :items, :pet_state, :alt_style
scope :with_pet_type_color_ids, ->(color_ids) {
joins(:pet_type).where(PetType.arel_table[:id].in(color_ids))
}
class ModelingDisabled < RuntimeError;end
def load!(options={})
raise ModelingDisabled
options[:locale] ||= I18n.default_locale
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
true
end
@ -32,43 +33,40 @@ class Pet < ApplicationRecord
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(
species_id: pet_data[:species_id].to_i,
color_id: pet_data[:color_id].to_i
)
self.pet_type.body_id = pet_data[:body_id]
self.pet_type.origin_pet = self
biology = pet_data[:biology_by_zone]
biology[0] = nil # remove effects if present
@pet_state = self.pet_type.add_pet_state_from_biology! biology
pet_state_biology = pet_data[:alt_style] ?
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,
viewer_data[:object_info_registry], viewer_data[:object_asset_registry],
options[:item_scope])
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
{
@ -81,7 +79,7 @@ class Pet < ApplicationRecord
end
def contributables
contributables = [pet_type, @pet_state]
contributables = [pet_type, @pet_state, @alt_style].filter(&:present?)
items.each do |item|
contributables << item
contributables += item.pending_swf_assets
@ -102,6 +100,10 @@ class Pet < ApplicationRecord
item.handle_assets!
end
end
if @alt_style
@alt_style.save!
end
end
def self.load(name, options={})
@ -116,7 +118,32 @@ class Pet < ApplicationRecord
pet
end
class PetNotFound < Exception;end
class DownloadError < Exception;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, 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

View file

@ -180,6 +180,15 @@ class SwfAsset < ApplicationRecord
self.manifest_url = parsed_manifest_url.to_s
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)
where((
arel_table[:remote_id].in(ids[:biology]).and(arel_table[:type].eq('biology'))

View file

@ -2,10 +2,12 @@
= advertise_campaign_progress @campaign
.warning
.notice
%strong Happy NC UC day!
We've temporarily disabled pet loading while we get everything set up and
investigate some new compatibility issues. We'll have it back soon!
We're still working on Alt Styles support, but other pets can be loaded as
usual!
%br
Excited to have them for you soon!
%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')
[503, {}, [PETS_THROTTLE_MESSAGE]]
else
flash = req.flash
flash = req.env['action_dispatch.request.flash_hash']
flash[:warning] = PETS_THROTTLE_MESSAGE
[302, {"Location" => "/"}, [PETS_THROTTLE_MESSAGE]]
end

View file

@ -232,6 +232,7 @@ en:
swf_asset_html: "%{item_description} on a new body type"
pet_type_html: "%{pet_type_description} for the first time"
pet_state_html: "a new pose for %{pet_type_description}"
alt_style_html: "a new Alt Style of the %{alt_style_name}"
contribution:
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
investigate some changes in how it works. We'll be back as soon as we
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:
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.
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|
t.string "short_name", limit: 10, 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
end
add_foreign_key "alt_styles", "colors"
add_foreign_key "alt_styles", "species"
end

View file

@ -1,8 +1,8 @@
require 'net/http'
require 'rocketamf'
require File.join(File.dirname(__FILE__), 'remote_gateway', 'service')
require_relative 'remote_gateway/service'
module RocketAMF
module RocketAMFExtensions
class RemoteGateway
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 Action
attr_reader :service, :name

View file

@ -1,6 +1,6 @@
require 'timeout'
module RocketAMF
module RocketAMFExtensions
class RemoteGateway
class Request
ERROR_CODE = 'AMFPHP_RUNTIME_ERROR'
@ -51,7 +51,7 @@ module RocketAMF
first_message_data = HashWithIndifferentAccess.new(result.messages[0].data)
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
result
@ -60,17 +60,17 @@ module RocketAMF
private
def envelope
output = Envelope.new
output = RocketAMF::Envelope.new
output.messages << wrapper_message
output
end
def wrapper_message
message = Message.new 'null', '/1', [remoting_message]
message = RocketAMF::Message.new 'null', '/1', [remoting_message]
end
def remoting_message
message = Values::RemotingMessage.new
message = RocketAMF::Values::RemotingMessage.new
message.source = @action.service.name
message.operation = @action.name
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 Service
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