ぼざクリタグ広場 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.
 
 
 
 
 
 

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