require 'open-uri' require 'nokogiri' class PostsController < ApplicationController # GET /posts def index limit = params[:limit].presence&.to_i cursor = params[:cursor].presence created_at = ('COALESCE(posts.original_created_before - INTERVAL 1 SECOND,' + 'posts.original_created_from,' + 'posts.created_at)') q = filtered_posts.order(Arel.sql("#{ created_at } DESC")) q = q.where("#{ created_at } < ?", Time.iso8601(cursor)) if cursor posts = limit ? q.limit(limit + 1) : q next_cursor = nil if limit && posts.size > limit next_cursor = posts.last.created_at.iso8601(6) posts = posts.first(limit) end render json: { posts: posts.map { |post| post.as_json(include: { tags: { only: [:id, :name, :category, :post_count] } }).tap do |json| json['thumbnail'] = if post.thumbnail.attached? rails_storage_proxy_url(post.thumbnail, only_path: false) else nil end end }, next_cursor: } end def random post = filtered_posts.order('RAND()').first return head :not_found unless post viewed = current_user&.viewed?(post) || false render json: (post .as_json(include: { tags: { only: [:id, :name, :category, :post_count] } }) .merge(viewed:)) end # GET /posts/1 def show post = Post.includes(:tags).find(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 # POST /posts def create return head :unauthorized unless current_user return head :forbidden unless current_user.member? # TODO: URL が正規のものがチェック,不正ならエラー # TODO: URL は必須にする(タイトルは省略可). # TODO: サイトに応じて thumbnail_base 設定 title = params[:title] 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: '', uploaded_user: current_user, original_created_from:, original_created_before:) post.thumbnail.attach(thumbnail) if post.save post.resized_thumbnail! post.tags = Tag.normalise_tags(tag_names) post.tags = Tag.expand_parent_tags(post.tags) render json: post.as_json(include: { tags: { only: [:id, :name, :category, :post_count] } }), status: :created else render json: { errors: post.errors.full_messages }, status: :unprocessable_entity end 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 # PATCH/PUT /posts/1 def update return head :unauthorized unless current_user return head :forbidden unless current_user.member? title = params[:title] 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) tags = post.tags.where(category: 'nico').to_a + Tag.normalise_tags(tag_names, with_tagme: false) tags = Tag.expand_parent_tags(tags) if post.update(title:, tags:, original_created_from:, original_created_before:) 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 end # DELETE /posts/1 def destroy end private def filtered_posts tag_names = params[:tags]&.split(' ') match_type = params[:match] tag_names.present? ? filter_posts_by_tags(tag_names, match_type) : Post.all end def filter_posts_by_tags tag_names, match_type posts = Post.joins(:tags) if match_type == 'any' posts = posts.where(tags: { name: tag_names }).distinct else tag_names.each do |tag| posts = posts.where(id: Post.joins(:tags).where(tags: { name: tag })) end end posts.distinct 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 tag.as_json(only: [:id, :name, :category, :post_count]).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] = tag.as_json(only: [:id, :name, :category, :post_count]).merge(children:) end root_ids.filter_map { |id| build_node.call(id, []) } end end