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

306 lines
8.8 KiB

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