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

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