hensei-api/app/controllers/api/v1/import_controller.rb

346 lines
11 KiB
Ruby

# frozen_string_literal: true
module Api
module V1
##
# ImportController is responsible for importing game data (e.g. deck data)
# and creating a new Party along with associated records (job, characters, weapons, summons, etc.).
#
# The controller expects a JSON payload whose top-level key is "import". If not wrapped,
# the controller will wrap the raw data automatically.
#
# @example Valid payload structure
# {
# "import": {
# "deck": { "name": "My Party", ... },
# "pc": { "job": { "master": { "name": "Warrior" } }, ... }
# }
# }
class ImportController < Api::V1::ApiController
ELEMENT_MAPPING = {
0 => nil,
1 => 4,
2 => 2,
3 => 3,
4 => 1,
5 => 6,
6 => 5
}.freeze
# GBF series_id to CharacterSeries slug mapping
GBF_SERIES_TO_SLUG = {
1 => 'summer',
2 => 'yukata',
3 => 'valentine',
4 => 'halloween',
5 => 'holiday',
6 => 'zodiac',
7 => 'grand',
8 => 'fantasy',
9 => 'collab',
10 => 'eternal',
11 => 'evoker',
12 => 'saint',
13 => 'formal'
}.freeze
# GBF series_id to WeaponSeries slug mapping
GBF_WEAPON_SERIES_TO_SLUG = {
1 => 'seraphic',
2 => 'grand',
3 => 'dark-opus',
4 => 'revenant',
5 => 'primal',
6 => 'beast',
7 => 'regalia',
8 => 'omega',
9 => 'olden-primal',
10 => 'hollowsky',
11 => 'xeno',
12 => 'rose',
13 => 'ultima',
14 => 'bahamut',
15 => 'epic',
16 => 'cosmos',
17 => 'superlative',
18 => 'vintage',
19 => 'class-champion',
20 => 'replica',
21 => 'relic',
22 => 'rusted',
23 => 'sephira',
24 => 'vyrmament',
25 => 'upgrader',
26 => 'astral',
27 => 'draconic',
28 => 'eternal-splendor',
29 => 'ancestral',
30 => 'new-world-foundation',
31 => 'ennead',
32 => 'militis',
33 => 'malice',
34 => 'menace',
35 => 'illustrious',
36 => 'proven',
37 => 'revans',
38 => 'world',
39 => 'exo',
40 => 'draconic-providence',
41 => 'celestial',
42 => 'omega-rebirth',
43 => 'collab',
44 => 'destroyer'
}.freeze
# GBF series_id to SummonSeries slug mapping
GBF_SUMMON_SERIES_TO_SLUG = {
1 => 'providence',
2 => 'genesis',
3 => 'magna',
4 => 'optimus',
5 => 'demi-optimus',
6 => 'archangel',
7 => 'arcarum',
8 => 'epic',
9 => 'carbuncle',
10 => 'dynamis',
12 => 'cryptid',
13 => 'six-dragons',
14 => 'summer',
15 => 'yukata',
16 => 'holiday',
17 => 'collab',
18 => 'bellum',
19 => 'crest',
20 => 'robur'
}.freeze
before_action :ensure_admin_role, only: %i[weapons summons characters]
##
# Processes an import request.
#
# It reads and parses the raw JSON, wraps the data under the "import" key if necessary,
# transforms the deck data using BaseDeckTransformer, validates that the transformed data
# contains required fields, and then creates a new Party record (and its associated objects)
# inside a transaction.
#
# @return [void] Renders JSON response with a party shortcode or an error message.
def create
Rails.logger.info '[IMPORT] Checking input...'
body = parse_request_body
return unless body
raw_params = body['import']
unless raw_params.is_a?(Hash)
Rails.logger.error "[IMPORT] 'import' key is missing or not a hash."
return render json: { error: 'Invalid JSON data' }, status: :unprocessable_content
end
unless raw_params['deck'].is_a?(Hash) &&
raw_params['deck'].key?('pc') &&
raw_params['deck'].key?('npc')
Rails.logger.error '[IMPORT] Deck data incomplete or missing.'
return render json: { error: 'Invalid deck data' }, status: :unprocessable_content
end
Rails.logger.info '[IMPORT] Starting import...'
return if performed? # Rendered an error response already
party = Party.create(user: current_user)
deck_data = raw_params['import']
process_data(party, deck_data)
render json: { shortcode: party.shortcode }, status: :created
rescue StandardError => e
render json: { error: e.message }, status: :unprocessable_content
end
def weapons
Rails.logger.info '[IMPORT] Checking weapon gamedata input...'
body = parse_request_body
return unless body
weapon = Weapon.find_by(granblue_id: body['id'])
unless weapon
Rails.logger.error "[IMPORT] Weapon not found with ID: #{body['id']}"
return render json: { error: 'Weapon not found' }, status: :not_found
end
lang = params[:lang]
unless %w[en jp].include?(lang)
Rails.logger.error "[IMPORT] Invalid language: #{lang}"
return render json: { error: 'Invalid language' }, status: :unprocessable_content
end
begin
weapon.update!(
"game_raw_#{lang}" => body.to_json
)
# Parse series_id and assign WeaponSeries
series_id = body['series_id'] || body.dig('master', 'series_id')
if series_id
slug = GBF_WEAPON_SERIES_TO_SLUG[series_id.to_i]
if slug
series_record = WeaponSeries.find_by(slug: slug)
if series_record && weapon.weapon_series != series_record
weapon.update!(weapon_series: series_record)
Rails.logger.info "[IMPORT] Set series '#{slug}' for weapon #{weapon.granblue_id}"
end
end
end
render json: { message: 'Weapon gamedata updated successfully' }, status: :ok
rescue StandardError => e
Rails.logger.error "[IMPORT] Failed to update weapon gamedata: #{e.message}"
render json: { error: e.message }, status: :unprocessable_content
end
end
def summons
Rails.logger.info '[IMPORT] Checking summon gamedata input...'
body = parse_request_body
return unless body
summon = Summon.find_by(granblue_id: body['id'])
unless summon
Rails.logger.error "[IMPORT] Summon not found with ID: #{body['id']}"
return render json: { error: 'Summon not found' }, status: :not_found
end
lang = params[:lang]
unless %w[en jp].include?(lang)
Rails.logger.error "[IMPORT] Invalid language: #{lang}"
return render json: { error: 'Invalid language' }, status: :unprocessable_content
end
begin
summon.update!(
"game_raw_#{lang}" => body.to_json
)
# Parse series_id and assign SummonSeries
series_id = body['series_id'] || body.dig('master', 'series_id')
if series_id
slug = GBF_SUMMON_SERIES_TO_SLUG[series_id.to_i]
if slug
series_record = SummonSeries.find_by(slug: slug)
if series_record && summon.summon_series != series_record
summon.update!(summon_series: series_record)
Rails.logger.info "[IMPORT] Set series '#{slug}' for summon #{summon.granblue_id}"
end
end
end
render json: { message: 'Summon gamedata updated successfully' }, status: :ok
rescue StandardError => e
Rails.logger.error "[IMPORT] Failed to update summon gamedata: #{e.message}"
render json: { error: e.message }, status: :unprocessable_content
end
end
##
# Updates character gamedata from JSON blob.
#
# @return [void] Renders JSON response with success or error message.
def characters
Rails.logger.info '[IMPORT] Checking character gamedata input...'
body = parse_request_body
return unless body
character = Character.find_by(granblue_id: body['id'])
unless character
Rails.logger.error "[IMPORT] Character not found with ID: #{body['id']}"
return render json: { error: 'Character not found' }, status: :not_found
end
lang = params[:lang]
unless %w[en jp].include?(lang)
Rails.logger.error "[IMPORT] Invalid language: #{lang}"
return render json: { error: 'Invalid language' }, status: :unprocessable_content
end
begin
character.update!(
"game_raw_#{lang}" => body.to_json
)
# Parse series_id and create CharacterSeriesMembership
series_id = body['series_id'] || body.dig('master', 'series_id')
if series_id
slug = GBF_SERIES_TO_SLUG[series_id.to_i]
if slug
series_record = CharacterSeries.find_by(slug: slug)
if series_record && !character.character_series_records.include?(series_record)
character.character_series_memberships.create!(character_series: series_record)
Rails.logger.info "[IMPORT] Added series '#{slug}' to character #{character.granblue_id}"
end
end
end
render json: { message: 'Character gamedata updated successfully' }, status: :ok
rescue StandardError => e
Rails.logger.error "[IMPORT] Failed to update character gamedata: #{e.message}"
render json: { error: e.message }, status: :unprocessable_content
end
end
private
##
# Ensures the current user has admin role (role 9).
# Renders an error if the user is not an admin.
#
# @return [void]
def ensure_admin_role
return if current_user&.role == 9
Rails.logger.error "[IMPORT] Unauthorized access attempt by user #{current_user&.id}"
render json: { error: 'Unauthorized' }, status: :unauthorized
end
##
# Reads and parses the raw JSON request body.
#
# @return [Hash] Parsed JSON data.
# @raise [JSON::ParserError] If the JSON is invalid.
def parse_request_body
raw_body = request.raw_post
JSON.parse(raw_body)
rescue JSON::ParserError => e
Rails.logger.error "[IMPORT] Invalid JSON: #{e.message}"
render json: { error: 'Invalid JSON data' }, status: :bad_request and return
end
##
# Ensures that the provided data is wrapped under an "import" key.
#
# @param data [Hash] The parsed JSON data.
# @return [Hash] Data wrapped under the "import" key.
def wrap_import_data(data)
data.key?('import') ? data : { 'import' => data }
end
##
# Processes the deck data using processors.
#
# @param party [Party] The party to insert data into
# @param data [Hash] The wrapped data.
# @return [Hash] The transformed deck data.
def process_data(party, data)
Rails.logger.info '[IMPORT] Transforming deck data'
Processors::JobProcessor.new(party, data).process
Processors::CharacterProcessor.new(party, data).process
Processors::SummonProcessor.new(party, data).process
Processors::WeaponProcessor.new(party, data).process
end
end
end
end