このコミットが含まれているのは:
@@ -115,7 +115,7 @@ class PostsController < ApplicationController
|
||||
render json: PostRepr.base(post, current_user)
|
||||
end
|
||||
|
||||
def preview
|
||||
def metadata
|
||||
return head :unauthorized unless current_user
|
||||
return head :forbidden unless current_user.gte_member?
|
||||
return render_bad_request('URL は必須です.') if params[:url].blank?
|
||||
@@ -128,6 +128,7 @@ class PostsController < ApplicationController
|
||||
provenance: { },
|
||||
tag_sources: { } }],
|
||||
fetch_metadata: true).first
|
||||
return render_validation_error fields: preview[:validation_errors] if preview[:validation_errors].present?
|
||||
|
||||
render json: {
|
||||
url: preview[:url],
|
||||
@@ -140,7 +141,6 @@ class PostsController < ApplicationController
|
||||
video_ms: preview[:attributes]['video_ms'],
|
||||
field_warnings: preview[:field_warnings],
|
||||
base_warnings: preview[:base_warnings],
|
||||
validation_errors: preview[:validation_errors],
|
||||
existing_post: compact_post(preview[:existing_post_id]) }
|
||||
rescue ArgumentError => e
|
||||
render_bad_request e.message
|
||||
@@ -178,6 +178,11 @@ class PostsController < ApplicationController
|
||||
attributes: post_create_attributes,
|
||||
thumbnail: params[:thumbnail]).run
|
||||
return render json: dry_run_json(preflight) if bool?(:dry)
|
||||
if preflight[:existing_post].present?
|
||||
post = Post.new(url: preflight[:url])
|
||||
post.errors.add :url, :taken
|
||||
return render_post_form_record_invalid post
|
||||
end
|
||||
|
||||
post = PostCreator.new(actor: current_user,
|
||||
attributes: post_create_attributes.merge(
|
||||
@@ -191,7 +196,10 @@ class PostsController < ApplicationController
|
||||
:original_created_before,
|
||||
:duration,
|
||||
:video_ms,
|
||||
:normalised_tags,
|
||||
:direct_tag_specs,
|
||||
:default_tag_specs,
|
||||
:snapshot_tag_specs,
|
||||
:post_tag_specs,
|
||||
:tag_sections,
|
||||
:normalised_parent_post_ids).symbolize_keys).merge(
|
||||
thumbnail: params[:thumbnail])).create!
|
||||
|
||||
@@ -12,30 +12,32 @@ class PostBulkCreator
|
||||
|
||||
workers = Array.new(2) do
|
||||
Thread.new do
|
||||
ActiveRecord::Base.connection_pool.with_connection do
|
||||
actor = User.find(@actor_id)
|
||||
loop do
|
||||
index = nil
|
||||
begin
|
||||
index = mutex.synchronize do
|
||||
current = next_index
|
||||
next_index += 1
|
||||
current
|
||||
end
|
||||
break if index >= @posts.length
|
||||
Rails.application.executor.wrap do
|
||||
ActiveRecord::Base.connection_pool.with_connection do
|
||||
actor = User.find(@actor_id)
|
||||
loop do
|
||||
index = nil
|
||||
begin
|
||||
index = mutex.synchronize do
|
||||
current = next_index
|
||||
next_index += 1
|
||||
current
|
||||
end
|
||||
break if index >= @posts.length
|
||||
|
||||
attributes = @posts[index]
|
||||
results[index] = create_row(actor, attributes, index)
|
||||
rescue StandardError => e
|
||||
Rails.logger.error(
|
||||
"post_bulk_creator_worker_failure #{ { error: e.class.name,
|
||||
message: e.message,
|
||||
index: }.to_json }")
|
||||
results[index] = {
|
||||
status: 'failed',
|
||||
recoverable: false,
|
||||
errors: { base: ['登録中にエラーが発生しました.'] },
|
||||
base_errors: [] }
|
||||
attributes = @posts[index]
|
||||
results[index] = create_row(actor, attributes, index)
|
||||
rescue StandardError => e
|
||||
Rails.logger.error(
|
||||
"post_bulk_creator_worker_failure #{ { error: e.class.name,
|
||||
message: e.message,
|
||||
index: }.to_json }")
|
||||
results[index] = {
|
||||
status: 'failed',
|
||||
recoverable: false,
|
||||
errors: { base: ['登録中にエラーが発生しました.'] },
|
||||
base_errors: [] }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -43,6 +45,16 @@ class PostBulkCreator
|
||||
end
|
||||
workers.each(&:join)
|
||||
|
||||
results.each_index do |index|
|
||||
next if results[index].present?
|
||||
|
||||
results[index] = {
|
||||
status: 'failed',
|
||||
recoverable: false,
|
||||
errors: { base: ['登録中にエラーが発生しました.'] },
|
||||
base_errors: [] }
|
||||
end
|
||||
|
||||
{ results: }
|
||||
end
|
||||
|
||||
@@ -156,7 +168,10 @@ class PostBulkCreator
|
||||
original_created_before: preflight[:original_created_before],
|
||||
duration: preflight[:duration],
|
||||
video_ms: preflight[:video_ms],
|
||||
normalised_tags: preflight[:normalised_tags],
|
||||
direct_tag_specs: preflight[:direct_tag_specs],
|
||||
default_tag_specs: preflight[:default_tag_specs],
|
||||
snapshot_tag_specs: preflight[:snapshot_tag_specs],
|
||||
post_tag_specs: preflight[:post_tag_specs],
|
||||
tag_sections: preflight[:tag_sections],
|
||||
normalised_parent_post_ids: preflight[:normalised_parent_post_ids] }
|
||||
end
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
class PostCreatePlan
|
||||
VIDEO_TAG_NAME = '動画'.freeze
|
||||
TAGME_TAG_NAME = 'タグ希望'.freeze
|
||||
NO_DEERJIKIST_TAG_NAME = 'ニジラー情報不詳'.freeze
|
||||
|
||||
def initialize attributes:
|
||||
@attributes = attributes.symbolize_keys
|
||||
@existing_tags_by_name = nil
|
||||
end
|
||||
|
||||
def build!
|
||||
Tag.normalise_tags!(tag_names, with_tagme: false, deny_deprecated: true,
|
||||
with_sections: true) =>
|
||||
{ tags:, sections: }
|
||||
|
||||
tags = Tag.expand_parent_tags(tags).reject(&:deprecated?)
|
||||
video_ms = normalise_video_ms(tags)
|
||||
validate_video_sections!(video_ms, sections)
|
||||
direct_tag_specs, tag_sections = parse_direct_tag_specs
|
||||
default_tag_specs = build_default_tag_specs(direct_tag_specs)
|
||||
snapshot_tag_specs = merge_tag_specs(direct_tag_specs + default_tag_specs)
|
||||
post_tag_specs = expand_parent_tag_specs(snapshot_tag_specs)
|
||||
video_ms = normalise_video_ms(snapshot_tag_specs)
|
||||
validate_video_sections!(video_ms, tag_sections)
|
||||
parent_post_ids = normalise_parent_post_ids
|
||||
validate_parent_post_ids!(parent_post_ids)
|
||||
|
||||
@@ -20,12 +24,15 @@ class PostCreatePlan
|
||||
thumbnail_base: @attributes[:thumbnail_base].presence,
|
||||
original_created_from: @attributes[:original_created_from].presence,
|
||||
original_created_before: @attributes[:original_created_before].presence,
|
||||
tags: serialised_tags(tags, sections),
|
||||
tags: serialised_tags(direct_tag_specs, tag_sections),
|
||||
duration: @attributes[:duration].to_s,
|
||||
video_ms: video_ms,
|
||||
parent_post_ids: parent_post_ids.join(' '),
|
||||
normalised_tags: tags,
|
||||
tag_sections: sections,
|
||||
direct_tag_specs: direct_tag_specs,
|
||||
default_tag_specs: default_tag_specs,
|
||||
snapshot_tag_specs: snapshot_tag_specs,
|
||||
post_tag_specs: post_tag_specs,
|
||||
tag_sections: tag_sections,
|
||||
normalised_parent_post_ids: parent_post_ids }
|
||||
end
|
||||
|
||||
@@ -33,6 +40,112 @@ class PostCreatePlan
|
||||
|
||||
def tag_names = @attributes[:tags].to_s.split
|
||||
|
||||
def parse_direct_tag_specs
|
||||
tag_sections = { }
|
||||
direct_tag_specs = []
|
||||
|
||||
tag_names.each do |raw_name|
|
||||
tag_name, category, sections = parse_raw_tag_name(raw_name)
|
||||
existing_tag = existing_tags_by_name[tag_name]
|
||||
raise Tag::NicoTagNormalisationError if existing_tag&.nico?
|
||||
raise Tag::DeprecatedTagNormalisationError, [existing_tag.name] if existing_tag&.deprecated?
|
||||
|
||||
direct_tag_specs << {
|
||||
name: tag_name,
|
||||
category: (category || existing_tag&.category || 'general').to_sym }
|
||||
if sections.present?
|
||||
tag_sections[tag_name] ||= []
|
||||
tag_sections[tag_name].concat(sections)
|
||||
tag_sections[tag_name] = Tag.merge_section_ranges(tag_sections[tag_name])
|
||||
tag_sections.delete(tag_name) if tag_sections[tag_name] == [[0, nil]]
|
||||
end
|
||||
end
|
||||
|
||||
[merge_tag_specs(direct_tag_specs), tag_sections]
|
||||
end
|
||||
|
||||
def parse_raw_tag_name raw_name
|
||||
name = raw_name.to_s
|
||||
prefix, category =
|
||||
Tag::CATEGORY_PREFIXES.find {
|
||||
name.downcase.start_with?(_1[0])
|
||||
} || ['', nil]
|
||||
name = name.sub(/\A#{ prefix }/i, '')
|
||||
|
||||
sections = []
|
||||
while (match = name.match(/\A(\S*?)\[([^\[\]\s]*)-([^\[\]\s]*)\](\S*)\z/))
|
||||
name = "#{ match[1] }#{ match[4] }"
|
||||
next if match[2].empty? && match[3].empty?
|
||||
|
||||
sections << Tag.normalise_section_range!(
|
||||
begin_raw: match[2],
|
||||
end_raw: match[3],
|
||||
tag_name: name)
|
||||
end
|
||||
raise Tag::SectionLiteralParseError.new(raw_name, raw_name) if name.include?('[') || name.include?(']')
|
||||
|
||||
[TagName.canonicalise(name).first, category&.to_sym, sections]
|
||||
end
|
||||
|
||||
def build_default_tag_specs direct_tag_specs
|
||||
default_tag_specs = []
|
||||
if direct_tag_specs.length < 10 && direct_tag_specs.none? { _1[:name] == TAGME_TAG_NAME }
|
||||
default_tag_specs << {
|
||||
name: TAGME_TAG_NAME,
|
||||
category: :meta }
|
||||
end
|
||||
if direct_tag_specs.none? { deerjikist_tag_spec?(_1) }
|
||||
default_tag_specs << {
|
||||
name: NO_DEERJIKIST_TAG_NAME,
|
||||
category: :meta }
|
||||
end
|
||||
|
||||
default_tag_specs
|
||||
end
|
||||
|
||||
def expand_parent_tag_specs snapshot_tag_specs
|
||||
existing_snapshot_tags = snapshot_tag_specs.filter_map { existing_tags_by_name[_1[:name]] }
|
||||
expanded_parent_specs =
|
||||
Tag.expand_parent_tags(existing_snapshot_tags)
|
||||
.reject(&:deprecated?)
|
||||
.map { |tag|
|
||||
{
|
||||
name: tag.name,
|
||||
category: tag.category.to_sym } }
|
||||
merge_tag_specs(snapshot_tag_specs + expanded_parent_specs)
|
||||
end
|
||||
|
||||
def merge_tag_specs specs
|
||||
specs.each_with_object({ }) do |spec, merged|
|
||||
merged[spec[:name]] =
|
||||
if merged.key?(spec[:name]) && merged[spec[:name]][:category] != :general
|
||||
merged[spec[:name]]
|
||||
else
|
||||
{
|
||||
name: spec[:name],
|
||||
category: spec[:category] }
|
||||
end
|
||||
end.values.sort_by { _1[:name] }
|
||||
end
|
||||
|
||||
def existing_tags_by_name
|
||||
@existing_tags_by_name ||= begin
|
||||
names = tag_names.map { canonical_tag_name_without_sections(_1) }.uniq
|
||||
Tag.joins(:tag_name).where(tag_names: { name: names }).index_by(&:name)
|
||||
end
|
||||
end
|
||||
|
||||
def canonical_tag_name_without_sections raw_name
|
||||
name, = parse_raw_tag_name(raw_name)
|
||||
name
|
||||
end
|
||||
|
||||
def deerjikist_tag_spec? spec
|
||||
return true if spec[:category] == :deerjikist
|
||||
|
||||
existing_tags_by_name[spec[:name]]&.deerjikist?
|
||||
end
|
||||
|
||||
def normalise_parent_post_ids
|
||||
Array(@attributes[:parent_post_ids]).flat_map { _1.to_s.split }.map { |token|
|
||||
id = Integer(token, exception: false)
|
||||
@@ -47,14 +160,14 @@ class PostCreatePlan
|
||||
raise ArgumentError, "存在しない親投稿 Id. があります: #{ missing.join(' ') }" if missing.present?
|
||||
end
|
||||
|
||||
def serialised_tags tags, sections
|
||||
tags.uniq(&:id).map { |tag|
|
||||
"#{ tag.name }#{ sections[tag.id].to_a.map { Post.section_literal(_1) }.join }"
|
||||
def serialised_tags direct_tag_specs, tag_sections
|
||||
direct_tag_specs.map { |spec|
|
||||
"#{ spec[:name] }#{ tag_sections[spec[:name]].to_a.map { Post.section_literal(_1) }.join }"
|
||||
}.sort.join(' ')
|
||||
end
|
||||
|
||||
def normalise_video_ms tags
|
||||
return nil unless tags.any? { _1.id == Tag.video.id }
|
||||
def normalise_video_ms snapshot_tag_specs
|
||||
return nil unless snapshot_tag_specs.any? { _1[:name] == VIDEO_TAG_NAME }
|
||||
|
||||
video_ms = @attributes[:video_ms]
|
||||
if video_ms.present?
|
||||
@@ -75,17 +188,18 @@ class PostCreatePlan
|
||||
raise PostCreator::VideoMsParseError
|
||||
end
|
||||
|
||||
def validate_video_sections! video_ms, sections
|
||||
def validate_video_sections! video_ms, tag_sections
|
||||
return unless video_ms
|
||||
|
||||
sections.each_value do |ranges|
|
||||
tag_sections.each_value do |ranges|
|
||||
ranges.each do |begin_ms, end_ms|
|
||||
post = Post.new
|
||||
if begin_ms >= video_ms
|
||||
post = Post.new
|
||||
post.errors.add :video_ms, 'タグ区間の開始が動画時間以上です.'
|
||||
raise ActiveRecord::RecordInvalid, post
|
||||
end
|
||||
if end_ms && end_ms > video_ms
|
||||
post = Post.new
|
||||
post.errors.add :video_ms, 'タグ区間の終端が動画時間を超えてゐます.'
|
||||
raise ActiveRecord::RecordInvalid, post
|
||||
end
|
||||
|
||||
@@ -62,7 +62,10 @@ class PostCreatePreflight
|
||||
original_created_before: plan[:original_created_before],
|
||||
duration: plan[:duration],
|
||||
video_ms: plan[:video_ms],
|
||||
normalised_tags: plan[:normalised_tags],
|
||||
direct_tag_specs: plan[:direct_tag_specs],
|
||||
default_tag_specs: plan[:default_tag_specs],
|
||||
snapshot_tag_specs: plan[:snapshot_tag_specs],
|
||||
post_tag_specs: plan[:post_tag_specs],
|
||||
tag_sections: plan[:tag_sections],
|
||||
normalised_parent_post_ids: plan[:normalised_parent_post_ids],
|
||||
field_warnings: final_field_warnings(preview[:field_warnings] || { }),
|
||||
@@ -126,8 +129,8 @@ class PostCreatePreflight
|
||||
thumbnail_warnings = (thumbnail_warnings + ['サムネールなし']).uniq
|
||||
end
|
||||
|
||||
{
|
||||
**field_warnings,
|
||||
'thumbnail_base' => thumbnail_warnings }
|
||||
next_warnings = field_warnings.except('thumbnail_base')
|
||||
next_warnings['thumbnail_base'] = thumbnail_warnings if thumbnail_warnings.present?
|
||||
next_warnings
|
||||
end
|
||||
end
|
||||
|
||||
@@ -21,12 +21,13 @@ class PostCreator
|
||||
ApplicationRecord.transaction do
|
||||
post.save!
|
||||
post.thumbnail.attach(thumbnail_attachment) if thumbnail_attachment.present?
|
||||
tags = planned_tags
|
||||
snapshot_tags = planned_snapshot_tags
|
||||
post_tags = planned_post_tags
|
||||
sections = planned_sections
|
||||
TagVersioning.record_tag_snapshots!(tags, created_by_user: @actor)
|
||||
TagVersioning.record_tag_snapshots!(snapshot_tags, created_by_user: @actor)
|
||||
post.video_ms = planned_video_ms
|
||||
post.save!
|
||||
sync_post_tags!(post, tags, sections)
|
||||
sync_post_tags!(post, post_tags, sections)
|
||||
sync_parent_posts!(post, planned_parent_post_ids)
|
||||
PostVersionRecorder.record!(post:, event_type: :create, created_by_user: @actor)
|
||||
end
|
||||
@@ -44,7 +45,9 @@ class PostCreator
|
||||
thumbnail_base: @attributes[:thumbnail_base].presence)
|
||||
end
|
||||
|
||||
def planned_tags = planned_create_attributes[:normalised_tags]
|
||||
def planned_snapshot_tags = planned_create_attributes[:snapshot_tags]
|
||||
|
||||
def planned_post_tags = planned_create_attributes[:post_tags]
|
||||
|
||||
def planned_sections = planned_create_attributes[:tag_sections]
|
||||
|
||||
@@ -56,18 +59,65 @@ class PostCreator
|
||||
|
||||
def planned_create_attributes
|
||||
@planned_create_attributes ||= begin
|
||||
if @attributes.key?(:normalised_tags)
|
||||
if @attributes.key?(:snapshot_tag_specs)
|
||||
snapshot_tags = materialise_tags(@attributes[:snapshot_tag_specs] || [])
|
||||
post_tags = materialise_tags(@attributes[:post_tag_specs] || [])
|
||||
{
|
||||
normalised_tags: @attributes[:normalised_tags],
|
||||
tag_sections: @attributes[:tag_sections] || { },
|
||||
snapshot_tags: snapshot_tags,
|
||||
post_tags: post_tags,
|
||||
tag_sections: materialise_sections(
|
||||
@attributes[:tag_sections] || { },
|
||||
snapshot_tags,
|
||||
post_tags),
|
||||
normalised_parent_post_ids: @attributes[:normalised_parent_post_ids] || [],
|
||||
video_ms: @attributes[:video_ms] }
|
||||
else
|
||||
PostCreatePlan.new(attributes: @attributes).build!
|
||||
build_materialised_plan
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def build_materialised_plan
|
||||
plan = PostCreatePlan.new(attributes: @attributes).build!
|
||||
snapshot_tags = materialise_tags(plan[:snapshot_tag_specs] || [])
|
||||
post_tags = materialise_tags(plan[:post_tag_specs] || [])
|
||||
{
|
||||
snapshot_tags: snapshot_tags,
|
||||
post_tags: post_tags,
|
||||
tag_sections: materialise_sections(
|
||||
plan[:tag_sections] || { },
|
||||
snapshot_tags,
|
||||
post_tags),
|
||||
normalised_parent_post_ids: plan[:normalised_parent_post_ids] || [],
|
||||
video_ms: plan[:video_ms] }
|
||||
end
|
||||
|
||||
def materialise_tags specs
|
||||
Array(specs).each_with_object({ }) do |spec, tags|
|
||||
name = spec[:name] || spec['name']
|
||||
category = spec[:category] || spec['category']
|
||||
next if name.blank? || category.blank?
|
||||
|
||||
tag = Tag.find_or_create_by_tag_name!(name, category:)
|
||||
tag.update!(category:) if tag.category.to_sym != category.to_sym
|
||||
tags[name] ||= tag
|
||||
end.values
|
||||
end
|
||||
|
||||
def materialise_sections sections_by_name, snapshot_tags, post_tags
|
||||
tags_by_name = post_tags.index_by(&:name)
|
||||
snapshot_tags.each do |tag|
|
||||
tags_by_name[tag.name] ||= tag
|
||||
end
|
||||
|
||||
sections_by_name.each_with_object({ }) do |(tag_name, ranges), sections|
|
||||
tag = tags_by_name[tag_name.to_s]
|
||||
next if tag.nil?
|
||||
|
||||
sections[tag.id] = Array(ranges).map { |range| [range[0], range[1]] }
|
||||
end
|
||||
end
|
||||
|
||||
def sync_post_tags! post, desired_tags, sections
|
||||
desired_ids = desired_tags.map(&:id).to_set
|
||||
current_ids = post.tags.pluck(:id).to_set
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
class PostThumbnailUploadValidator
|
||||
MAX_THUMBNAIL_BYTES = 20 * 1024 * 1024
|
||||
ALLOWED_CONTENT_TYPES = Preview::ThumbnailFetcher::ALLOWED_IMAGE_CONTENT_TYPES.freeze
|
||||
|
||||
class InvalidUpload < StandardError; end
|
||||
|
||||
@@ -7,6 +8,7 @@ class PostThumbnailUploadValidator
|
||||
return if thumbnail.blank?
|
||||
raise InvalidUpload, 'thumbnail upload が不正です.' unless thumbnail.is_a?(ActionDispatch::Http::UploadedFile)
|
||||
raise InvalidUpload, 'thumbnail file size が大きすぎます.' if thumbnail.size > MAX_THUMBNAIL_BYTES
|
||||
raise InvalidUpload, 'サムネイル画像の形式が不正です.' unless allowed_content_type?(thumbnail.content_type)
|
||||
|
||||
attachment = Post.resized_thumbnail_attachment(thumbnail)
|
||||
attachment[:io].close if attachment[:io].respond_to?(:close)
|
||||
@@ -15,4 +17,9 @@ class PostThumbnailUploadValidator
|
||||
ensure
|
||||
thumbnail&.rewind if thumbnail.respond_to?(:rewind)
|
||||
end
|
||||
|
||||
def self.allowed_content_type? content_type
|
||||
mime_type = content_type.to_s.split(';', 2).first.to_s.downcase.strip
|
||||
ALLOWED_CONTENT_TYPES.include?(mime_type)
|
||||
end
|
||||
end
|
||||
|
||||
新しい課題から参照
ユーザをブロックする