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

292 lines
8.3 KiB

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