class PostCreator class VideoMsParseError < ArgumentError; end attr_reader :field_warnings def initialize actor:, attributes: @actor = actor @attributes = attributes.symbolize_keys @field_warnings = { } end def create! thumbnail_attachment = prepare_thumbnail_attachment post = Post.new(title: @attributes[:title].presence, url: @attributes[:url], thumbnail_base: @attributes[:thumbnail_base].presence, uploaded_user: @actor, original_created_from: @attributes[:original_created_from].presence, original_created_before: @attributes[:original_created_before].presence) ApplicationRecord.transaction do post.save! post.thumbnail.attach(thumbnail_attachment) if thumbnail_attachment.present? snapshot_tags = planned_snapshot_tags post_tags = planned_post_tags sections = planned_sections TagVersioning.record_tag_snapshots!(snapshot_tags, created_by_user: @actor) post.video_ms = planned_video_ms post.save! 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 post rescue StandardError post&.thumbnail&.purge if post&.thumbnail&.attached? raise end private def prepare_thumbnail_attachment PostThumbnailAttachmentBuilder.build( thumbnail: @attributes[:thumbnail], thumbnail_base: @attributes[:thumbnail_base].presence) end 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] def planned_parent_post_ids = planned_create_attributes[:normalised_parent_post_ids] def planned_video_ms planned_create_attributes[:video_ms] end def planned_create_attributes @planned_create_attributes ||= begin if @attributes.key?(:snapshot_tag_specs) snapshot_tags = materialise_tags(@attributes[:snapshot_tag_specs] || []) post_tags = materialise_tags(@attributes[:post_tag_specs] || []) { 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 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 Tag.where(id: desired_ids - current_ids).find_each do |tag| PostTag.create_or_find_by!(post:, tag:, created_user: @actor) end PostTagSection.where(post_id: post.id).destroy_all sections.each do |tag_id, ranges| ranges.each do |begin_ms, end_ms| PostTagSection.create!(post_id: post.id, tag_id:, begin_ms:, end_ms:) end end PostTag.where(post_id: post.id, tag_id: (current_ids - desired_ids).to_a).kept.find_each do |post_tag| post_tag.discard_by!(@actor) end end def sync_parent_posts! post, ids if ids.include?(post.id) post.errors.add :parent_post_ids, '自分自身を親投稿にはできません.' raise ActiveRecord::RecordInvalid, post end missing = ids - Post.where(id: ids).pluck(:id) if missing.present? post.errors.add :parent_post_ids, "存在しない親投稿 Id. があります: #{ missing.join(' ') }" raise ActiveRecord::RecordInvalid, post end ids.each { |parent_post_id| PostImplication.create_or_find_by!(post:, parent_post_id:) } end end