class PostsController < ApplicationController Event = Struct.new(:post, :tag, :user, :change_type, :timestamp, keyword_init: true) def index url = params[:url].presence title = params[:title].presence original_created_from = params[:original_created_from].presence original_created_to = params[:original_created_to].presence created_between = params[:created_from].presence, params[:created_to].presence updated_between = params[:updated_from].presence, params[:updated_to].presence order = params[:order].to_s.split(':', 2).map(&:strip) unless order[0].in?(['title', 'url', 'original_created_at', 'created_at', 'updated_at']) order[0] = 'original_created_at' end unless order[1].in?(['asc', 'desc']) order[1] = if order[0].in?(['title', 'url']) 'asc' else 'desc' end end 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 pt_max_sql = PostTag .select('post_id, MAX(updated_at) AS max_updated_at') .group('post_id') .to_sql updated_at_all_sql = 'GREATEST(posts.updated_at,' + 'COALESCE(pt_max.max_updated_at, posts.updated_at))' q = filtered_posts .joins("LEFT JOIN (#{ pt_max_sql }) pt_max ON pt_max.post_id = posts.id") .reselect('posts.*', Arel.sql("#{ updated_at_all_sql } AS updated_at_all")) .preload(tags: { tag_name: :wiki_page }) .with_attached_thumbnail q = q.where('posts.url LIKE ?', "%#{ url }%") if url q = q.where('posts.title LIKE ?', "%#{ title }%") if title if original_created_from q = q.where('posts.original_created_before > ?', original_created_from) end if original_created_to q = q.where('posts.original_created_from <= ?', original_created_to) end q = q.where('posts.created_at >= ?', created_between[0]) if created_between[0] q = q.where('posts.created_at <= ?', created_between[1]) if created_between[1] if updated_between[0] q = q.where("#{ updated_at_all_sql } >= ?", updated_between[0]) end if updated_between[1] q = q.where("#{ updated_at_all_sql } <= ?", updated_between[1]) end sort_sql = if order[0] == 'original_created_at' 'COALESCE(posts.original_created_before - INTERVAL 1 MINUTE,' + 'posts.original_created_from,' + 'posts.created_at) ' + order[1] else "posts.#{ order[0] } #{ order[1] }" end posts = q.order(Arel.sql("#{ sort_sql }")).limit(limit).offset(offset).to_a q = q.except(:select, :order) render json: { posts: posts.map { |post| PostRepr.base(post).merge(updated_at: post.updated_at_all).tap do |json| json['thumbnail'] = if post.thumbnail.attached? rails_storage_proxy_url(post.thumbnail, only_path: false) else nil end end }, count: q.group_values.present? ? q.count.size : q.count } end def random post = filtered_posts.preload(tags: { tag_name: :wiki_page }) .order('RAND()') .first return head :not_found unless post viewed = current_user&.viewed?(post) || false render json: PostRepr.base(post).merge(viewed:) end def show post = Post.includes(tags: { tag_name: :wiki_page }).find_by(id: params[:id]) return head :not_found unless post viewed = current_user&.viewed?(post) || false json = post.as_json json['tags'] = build_tag_tree_for(post.tags) json['related'] = post.related(limit: 20) json['viewed'] = viewed render json: end def create return head :unauthorized unless current_user return head :forbidden unless current_user.member? # TODO: サイトに応じて thumbnail_base 設定 title = params[:title].presence url = params[:url] thumbnail = params[:thumbnail] tag_names = params[:tags].to_s.split original_created_from = params[:original_created_from] original_created_before = params[:original_created_before] post = Post.new(title:, url:, thumbnail_base: nil, uploaded_user: current_user, original_created_from:, original_created_before:) post.thumbnail.attach(thumbnail) if post.save post.resized_thumbnail! tags = Tag.normalise_tags(tag_names) tags = Tag.expand_parent_tags(tags) sync_post_tags!(post, tags) post.reload render json: PostRepr.base(post), status: :created else render json: { errors: post.errors.full_messages }, status: :unprocessable_entity end rescue Tag::NicoTagNormalisationError head :bad_request end def viewed return head :unauthorized unless current_user current_user.viewed_posts << Post.find(params[:id]) head :no_content end def unviewed return head :unauthorized unless current_user current_user.viewed_posts.delete(Post.find(params[:id])) head :no_content end def update return head :unauthorized unless current_user return head :forbidden unless current_user.member? title = params[:title].presence tag_names = params[:tags].to_s.split original_created_from = params[:original_created_from] original_created_before = params[:original_created_before] post = Post.find(params[:id].to_i) if post.update(title:, original_created_from:, original_created_before:) tags = post.tags.where(category: 'nico').to_a + Tag.normalise_tags(tag_names, with_tagme: false) tags = Tag.expand_parent_tags(tags) sync_post_tags!(post, tags) post.reload json = post.as_json json['tags'] = build_tag_tree_for(post.tags) render json:, status: :ok else render json: post.errors, status: :unprocessable_entity end rescue Tag::NicoTagNormalisationError head :bad_request end def changes id = params[:id].presence 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 pts = PostTag.with_discarded pts = pts.where(post_id: id) if id.present? pts = pts.includes(:post, :created_user, :deleted_user, tag: { tag_name: :wiki_page }) events = [] pts.each do |pt| tag = TagRepr.base(pt.tag) post = pt.post events << Event.new( post:, tag:, user: pt.created_user && { id: pt.created_user.id, name: pt.created_user.name }, change_type: 'add', timestamp: pt.created_at) if pt.discarded_at events << Event.new( post:, tag:, user: pt.deleted_user && { id: pt.deleted_user.id, name: pt.deleted_user.name }, change_type: 'remove', timestamp: pt.discarded_at) end end events.sort_by!(&:timestamp) events.reverse! render json: { changes: (events.slice(offset, limit) || []).as_json, count: events.size } end private def filtered_posts tag_names = params[:tags].to_s.split match_type = params[:match] if tag_names.present? filter_posts_by_tags(tag_names, match_type) else Post.all end end def filter_posts_by_tags tag_names, match_type tag_names = TagName.canonicalise(tag_names) posts = Post.joins(tags: :tag_name) if match_type == 'any' posts.where(tag_names: { name: tag_names }).distinct else posts.where(tag_names: { name: tag_names }) .group('posts.id') .having('COUNT(DISTINCT tag_names.id) = ?', tag_names.uniq.size) end end def sync_post_tags! post, desired_tags desired_tags.each do |t| t.save! if t.new_record? end desired_ids = desired_tags.map(&:id).to_set current_ids = post.tags.pluck(:id).to_set to_add = desired_ids - current_ids to_remove = current_ids - desired_ids Tag.where(id: to_add).find_each do |tag| begin PostTag.create!(post:, tag:, created_user: current_user) rescue ActiveRecord::RecordNotUnique ; end end PostTag.where(post_id: post.id, tag_id: to_remove.to_a).kept.find_each do |pt| pt.discard_by!(current_user) end end def build_tag_tree_for tags tags = tags.to_a tag_ids = tags.map(&:id) implications = TagImplication.where(parent_tag_id: tag_ids, tag_id: tag_ids) children_ids_by_parent = Hash.new { |h, k| h[k] = [] } implications.each do |imp| children_ids_by_parent[imp.parent_tag_id] << imp.tag_id end child_ids = children_ids_by_parent.values.flatten.uniq root_ids = tag_ids - child_ids tags_by_id = tags.index_by(&:id) memo = { } build_node = -> tag_id, path do tag = tags_by_id[tag_id] return nil unless tag if path.include?(tag_id) return TagRepr.base(tag).merge(children: []) end if memo.key?(tag_id) return memo[tag_id] end new_path = path + [tag_id] child_ids = children_ids_by_parent[tag_id] || [] children = child_ids.filter_map { |cid| build_node.(cid, new_path) } memo[tag_id] = TagRepr.base(tag).merge(children:) end root_ids.filter_map { |id| build_node.call(id, []) } end end