ぼざクリタグ広場 https://hub.nizika.monster
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

261 lines
7.4 KiB

  1. class PostsController < ApplicationController
  2. Event = Struct.new(:post, :tag, :user, :change_type, :timestamp, keyword_init: true)
  3. # GET /posts
  4. def index
  5. limit = params[:limit].presence&.to_i
  6. cursor = params[:cursor].presence
  7. created_at = ('COALESCE(posts.original_created_before - INTERVAL 1 SECOND,' +
  8. 'posts.original_created_from,' +
  9. 'posts.created_at)')
  10. q = filtered_posts.order(Arel.sql("#{ created_at } DESC"))
  11. q = q.where("#{ created_at } < ?", Time.iso8601(cursor)) if cursor
  12. posts = limit ? q.limit(limit + 1) : q
  13. next_cursor = nil
  14. if limit && posts.size > limit
  15. next_cursor = posts.last.created_at.iso8601(6)
  16. posts = posts.first(limit)
  17. end
  18. render json: { posts: posts.map { |post|
  19. post.as_json(include: { tags: { only: [:id, :name, :category, :post_count] } }).tap do |json|
  20. json['thumbnail'] =
  21. if post.thumbnail.attached?
  22. rails_storage_proxy_url(post.thumbnail, only_path: false)
  23. else
  24. nil
  25. end
  26. end
  27. }, next_cursor: }
  28. end
  29. def random
  30. post = filtered_posts.order('RAND()').first
  31. return head :not_found unless post
  32. viewed = current_user&.viewed?(post) || false
  33. render json: (post
  34. .as_json(include: { tags: { only: [:id, :name, :category, :post_count] } })
  35. .merge(viewed:))
  36. end
  37. # GET /posts/1
  38. def show
  39. post = Post.includes(:tags).find(params[:id])
  40. return head :not_found unless post
  41. viewed = current_user&.viewed?(post) || false
  42. json = post.as_json
  43. json['tags'] = build_tag_tree_for(post.tags)
  44. json['related'] = post.related(limit: 20)
  45. json['viewed'] = viewed
  46. render json:
  47. end
  48. # POST /posts
  49. def create
  50. return head :unauthorized unless current_user
  51. return head :forbidden unless current_user.member?
  52. # TODO: URL が正規のものがチェック,不正ならエラー
  53. # TODO: URL は必須にする(タイトルは省略可).
  54. # TODO: サイトに応じて thumbnail_base 設定
  55. title = params[:title]
  56. url = params[:url]
  57. thumbnail = params[:thumbnail]
  58. tag_names = params[:tags].to_s.split(' ')
  59. original_created_from = params[:original_created_from]
  60. original_created_before = params[:original_created_before]
  61. post = Post.new(title:, url:, thumbnail_base: '', uploaded_user: current_user,
  62. original_created_from:, original_created_before:)
  63. post.thumbnail.attach(thumbnail)
  64. if post.save
  65. post.resized_thumbnail!
  66. tags = Tag.normalise_tags(tag_names)
  67. tags = Tag.expand_parent_tags(tags)
  68. sync_post_tags!(post, tags)
  69. render json: post.as_json(include: { tags: { only: [:id, :name, :category, :post_count] } }),
  70. status: :created
  71. else
  72. render json: { errors: post.errors.full_messages }, status: :unprocessable_entity
  73. end
  74. end
  75. def viewed
  76. return head :unauthorized unless current_user
  77. current_user.viewed_posts << Post.find(params[:id])
  78. head :no_content
  79. end
  80. def unviewed
  81. return head :unauthorized unless current_user
  82. current_user.viewed_posts.delete(Post.find(params[:id]))
  83. head :no_content
  84. end
  85. # PATCH/PUT /posts/1
  86. def update
  87. return head :unauthorized unless current_user
  88. return head :forbidden unless current_user.member?
  89. title = params[:title]
  90. tag_names = params[:tags].to_s.split(' ')
  91. original_created_from = params[:original_created_from]
  92. original_created_before = params[:original_created_before]
  93. post = Post.find(params[:id].to_i)
  94. if post.update(title:, original_created_from:, original_created_before:)
  95. tags = post.tags.where(category: 'nico').to_a +
  96. Tag.normalise_tags(tag_names, with_tagme: false)
  97. tags = Tag.expand_parent_tags(tags)
  98. sync_post_tags!(post, tags)
  99. json = post.as_json
  100. json['tags'] = build_tag_tree_for(post.tags)
  101. render json:, status: :ok
  102. else
  103. render json: post.errors, status: :unprocessable_entity
  104. end
  105. end
  106. # DELETE /posts/1
  107. def destroy
  108. end
  109. def changes
  110. id = params[:id]
  111. page = (params[:page].presence || 1).to_i
  112. limit = (params[:limit].presence || 20).to_i
  113. page = 1 if page < 1
  114. limit = 1 if limit < 1
  115. offset = (page - 1) * limit
  116. pts = PostTag.with_discarded
  117. pts = pts.where(post_id: id) if id.present?
  118. pts = pts.includes(:post, :tag, :created_user, :deleted_user)
  119. events = []
  120. pts.each do |pt|
  121. events << Event.new(
  122. post: pt.post,
  123. tag: pt.tag,
  124. user: pt.created_user && { id: pt.created_user.id, name: pt.created_user.name },
  125. change_type: 'add',
  126. timestamp: pt.created_at)
  127. if pt.discarded_at
  128. events << Event.new(
  129. post: pt.post,
  130. tag: pt.tag,
  131. user: pt.deleted_user && { id: pt.deleted_user.id, name: pt.deleted_user.name },
  132. change_type: 'remove',
  133. timestamp: pt.discarded_at)
  134. end
  135. end
  136. events.sort_by!(&:timestamp)
  137. events.reverse!
  138. render json: { changes: events.slice(offset, limit).as_json, count: events.size }
  139. end
  140. private
  141. def filtered_posts
  142. tag_names = params[:tags]&.split(' ')
  143. match_type = params[:match]
  144. if tag_names.present?
  145. filter_posts_by_tags(tag_names, match_type)
  146. else
  147. Post.all
  148. end
  149. end
  150. def filter_posts_by_tags tag_names, match_type
  151. posts = Post.joins(:tags)
  152. if match_type == 'any'
  153. posts = posts.where(tags: { name: tag_names }).distinct
  154. else
  155. tag_names.each do |tag|
  156. posts = posts.where(id: Post.joins(:tags).where(tags: { name: tag }))
  157. end
  158. end
  159. posts.distinct
  160. end
  161. def sync_post_tags! post, desired_tags
  162. desired_tags.each do |t|
  163. t.save! if t.new_record?
  164. end
  165. desired_ids = desired_tags.map(&:id).to_set
  166. current_ids = post.tags.pluck(:id).to_set
  167. to_add = desired_ids - current_ids
  168. to_remove = current_ids - desired_ids
  169. Tag.where(id: to_add).find_each do |tag|
  170. begin
  171. PostTag.create!(post:, tag:, created_user: current_user)
  172. rescue ActiveRecord::RecordNotUnique
  173. ;
  174. end
  175. end
  176. PostTag.where(post_id: post.id, tag_id: to_remove.to_a).kept.find_each do |pt|
  177. pt.discard_by!(current_user)
  178. end
  179. end
  180. def build_tag_tree_for tags
  181. tags = tags.to_a
  182. tag_ids = tags.map(&:id)
  183. implications = TagImplication.where(parent_tag_id: tag_ids, tag_id: tag_ids)
  184. children_ids_by_parent = Hash.new { |h, k| h[k] = [] }
  185. implications.each do |imp|
  186. children_ids_by_parent[imp.parent_tag_id] << imp.tag_id
  187. end
  188. child_ids = children_ids_by_parent.values.flatten.uniq
  189. root_ids = tag_ids - child_ids
  190. tags_by_id = tags.index_by(&:id)
  191. memo = { }
  192. build_node = -> tag_id, path do
  193. tag = tags_by_id[tag_id]
  194. return nil unless tag
  195. if path.include?(tag_id)
  196. return tag.as_json(only: [:id, :name, :category, :post_count]).merge(children: [])
  197. end
  198. if memo.key?(tag_id)
  199. return memo[tag_id]
  200. end
  201. new_path = path + [tag_id]
  202. child_ids = children_ids_by_parent[tag_id] || []
  203. children = child_ids.filter_map { |cid| build_node.(cid, new_path) }
  204. memo[tag_id] = tag.as_json(only: [:id, :name, :category, :post_count]).merge(children:)
  205. end
  206. root_ids.filter_map { |id| build_node.call(id, []) }
  207. end
  208. end