Compare commits
12 commits
610177d3f5
...
e511bdc5e2
Author | SHA1 | Date | |
---|---|---|---|
e511bdc5e2 | |||
4e5023288e | |||
1933046809 | |||
5004142dfb | |||
ec3bb7dbe0 | |||
4888a9dcbe | |||
b24ed7facb | |||
b0e7f2ccd5 | |||
0406a32444 | |||
1057fdd3a9 | |||
c33f1cb767 | |||
c76e8cd2a9 |
17 changed files with 179 additions and 59 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
22
app/models/alt_style.rb
Normal 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
|
|
@ -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] ||= {}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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'))
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
11
db/migrate/20240124102340_create_alt_styles.rb
Normal file
11
db/migrate/20240124102340_create_alt_styles.rb
Normal 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
|
14
db/schema.rb
14
db/schema.rb
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
@ -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
|
|
@ -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
|
|
@ -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
6
lib/tasks/pets.rake
Normal 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
|
Loading…
Reference in a new issue