class PostCreator class VideoMsParseError < ArgumentError; end def initialize actor:, attributes: @actor = actor @attributes = attributes.symbolize_keys end def create! 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) thumbnail = @attributes[:thumbnail] post.thumbnail.attach(Post.resized_thumbnail_attachment(thumbnail)) if thumbnail.present? ApplicationRecord.transaction do post.save! Tag.normalise_tags!(tag_names, deny_deprecated: true, with_sections: true) => { tags:, sections: } TagVersioning.record_tag_snapshots!(tags, created_by_user: @actor) tags = Tag.expand_parent_tags(tags).reject(&:deprecated?) post.video_ms = normalise_video_ms(tags) validate_video_sections!(post.video_ms, sections) post.save! sync_post_tags!(post, tags, sections) sync_parent_posts!(post, parent_post_ids) PostVersionRecorder.record!(post:, event_type: :create, created_by_user: @actor) end post end private def tag_names = @attributes[:tags].to_s.split def parent_post_ids Array(@attributes[:parent_post_ids]).flat_map { _1.to_s.split }.map { |token| id = Integer(token, exception: false) raise ArgumentError, "親投稿 Id. が不正です: #{ token }" if id.nil? || id <= 0 id }.uniq end def normalise_video_ms tags return nil unless tags.any? { _1.id == Tag.video.id } video_ms = @attributes[:video_ms] if video_ms.present? value = Integer(video_ms, exception: false) raise VideoMsParseError unless value&.positive? return value end duration = @attributes[:duration] return nil if duration.blank? return duration.to_i if duration.is_a?(Numeric) && duration.to_i.positive? value = Tag.time_to_ms!(duration.to_s, tag_name: '動画時間') raise VideoMsParseError unless value.positive? value rescue Tag::SectionLiteralParseError raise VideoMsParseError end def validate_video_sections! video_ms, sections return unless video_ms sections.each_value do |ranges| ranges.each do |begin_ms, end_ms| next if begin_ms < video_ms && (!end_ms || end_ms <= video_ms) post = Post.new post.errors.add :video_ms, 'タグ区間が動画時間の範囲外です.' raise ActiveRecord::RecordInvalid, post end 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 { |begin_ms, end_ms| PostTagSection.create!(post_id: post.id, tag_id:, begin_ms:, end_ms:) } 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