class PostVersionsController < ApplicationController def index post_id = params[:post].presence tag_id = params[:tag].presence&.to_i external_tag_id = params[:external_tag].presence&.to_i return head :bad_request if tag_id && external_tag_id page = (params[:page].presence || 1).to_i limit = (params[:limit].presence || 20).to_i page = 1 if page < 1 limit = 1 if limit < 1 offset = (page - 1) * limit q = PostVersion.joins(<<~SQL.squish) LEFT JOIN post_versions prev ON prev.post_id = post_versions.post_id AND prev.version_no = post_versions.version_no - 1 SQL .select('post_versions.*', 'prev.title AS prev_title', 'prev.url AS prev_url', 'prev.thumbnail_base AS prev_thumbnail_base', 'prev.tags_json AS prev_tags_json', 'prev.video_ms AS prev_video_ms', 'prev.original_created_from AS prev_original_created_from', 'prev.original_created_before AS prev_original_created_before') q = q.where('post_versions.post_id = ?', post_id) if post_id if external_tag_id || (tag_id && !(Tag.exists?(id: tag_id))) q = q.where('JSON_CONTAINS(post_versions.tags_json,' + "JSON_OBJECT('external_tag_id', #{ external_tag_id || tag_id })) " + 'OR JSON_CONTAINS(prev.tags_json,' + "JSON_OBJECT('external_tag_id', #{ external_tag_id || tag_id }))") elsif tag_id q = q.where("JSON_CONTAINS(post_versions.tags_json, JSON_OBJECT('tag_id', #{ tag_id })) " + "OR JSON_CONTAINS(prev.tags_json, JSON_OBJECT('tag_id', #{ tag_id }))") end count = q.except(:select, :order, :limit, :offset).count versions = q.order(Arel.sql('post_versions.created_at DESC, post_versions.id DESC')) .limit(limit) .offset(offset) render json: { versions: serialise_versions(versions), count: } end private def serialise_versions rows rows = rows.to_a user_ids = rows.map(&:created_by_user_id).compact.uniq users_by_id = User.where(id: user_ids).pluck(:id, :name).to_h snapshots = rows.flat_map { |row| [normalise_json(row.tags_json), normalise_json(row.attributes['prev_tags_json']) || []] } external_tag_names = external_tag_names_for(snapshots) rows.map do |row| cur_tags = snapshot_tag_literals(normalise_json(row.tags_json), external_tag_names) prev_tags = snapshot_tag_literals( normalise_json(row.attributes['prev_tags_json']) || [], external_tag_names) { post_id: row.post_id, version_no: row.version_no, event_type: row.event_type, title: { current: row.title, prev: row.attributes['prev_title'] }, url: { current: row.url, prev: row.attributes['prev_url'] }, thumbnail: { current: nil, prev: nil }, thumbnail_base: { current: row.thumbnail_base, prev: row.attributes['prev_thumbnail_base'] }, video_ms: { current: row.video_ms, prev: row.attributes['prev_video_ms'] }, tags: build_version_tags(cur_tags, prev_tags), original_created_from: { current: row.original_created_from&.iso8601, prev: row.attributes['prev_original_created_from']&.iso8601 }, original_created_before: { current: row.original_created_before&.iso8601, prev: row.attributes['prev_original_created_before']&.iso8601 }, created_at: row.created_at.iso8601, created_by_user: if row.created_by_user_id { id: row.created_by_user_id, name: users_by_id[row.created_by_user_id] } end } end end def external_tag_names_for snapshots ids = snapshots.flatten.filter_map { _1['external_tag_id'] }.uniq names = ExternalTag.where(id: ids).pluck(:id, :platform, :name).to_h { |id, platform, name| [id, "#{ platform }:#{ name }"] } missing_ids = ids - names.keys NicoTagVersion .where(tag_id: missing_ids) .order(:tag_id, version_no: :desc) .pluck(:tag_id, :name) .each { |id, name| names[id] ||= name } names end def snapshot_tag_literals snapshots, external_tag_names snapshots .filter_map { |snapshot| if snapshot.key?('tag_id') [tag_category_order(snapshot['category']), Post.tag_snapshot_literal(snapshot)] elsif external_tag_names[snapshot['external_tag_id']] [6, external_tag_names[snapshot['external_tag_id']]] end } .sort_by { |order, name| [order, name.downcase] } .map(&:second) end def tag_category_order category ['deerjikist', 'meme', 'character', 'general', 'material', 'meta'].index(category) || 6 end def build_version_tags cur_tags, prev_tags (cur_tags | prev_tags).map do |name| type = if cur_tags.include?(name) && prev_tags.include?(name) 'context' elsif cur_tags.include?(name) 'added' else 'removed' end { name:, type: } end end end