ぼざクリタグ広場 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.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. PostRepr.base(post).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: if filtered_posts.group_values.present?
  42. filtered_posts.count.size
  43. else
  44. filtered_posts.count
  45. end, next_cursor: }
  46. end
  47. def random
  48. post = filtered_posts.preload(tags: { tag_name: :wiki_page })
  49. .order('RAND()')
  50. .first
  51. return head :not_found unless post
  52. viewed = current_user&.viewed?(post) || false
  53. render json: PostRepr.base(post).merge(viewed:)
  54. end
  55. def show
  56. post = Post.includes(tags: { tag_name: :wiki_page }).find(params[:id])
  57. return head :not_found unless post
  58. viewed = current_user&.viewed?(post) || false
  59. json = post.as_json
  60. json['tags'] = build_tag_tree_for(post.tags)
  61. json['related'] = post.related(limit: 20)
  62. json['viewed'] = viewed
  63. render json:
  64. end
  65. def create
  66. return head :unauthorized unless current_user
  67. return head :forbidden unless current_user.gte_member?
  68. # TODO: サイトに応じて thumbnail_base 設定
  69. title = params[:title].presence
  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: nil, 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. post.reload
  84. render json: PostRepr.base(post), status: :created
  85. else
  86. render json: { errors: post.errors.full_messages }, status: :unprocessable_entity
  87. end
  88. rescue Tag::NicoTagNormalisationError
  89. head :bad_request
  90. end
  91. def viewed
  92. return head :unauthorized unless current_user
  93. current_user.viewed_posts << Post.find(params[:id])
  94. head :no_content
  95. end
  96. def unviewed
  97. return head :unauthorized unless current_user
  98. current_user.viewed_posts.delete(Post.find(params[:id]))
  99. head :no_content
  100. end
  101. def update
  102. return head :unauthorized unless current_user
  103. return head :forbidden unless current_user.gte_member?
  104. title = params[:title].presence
  105. tag_names = params[:tags].to_s.split
  106. original_created_from = params[:original_created_from]
  107. original_created_before = params[:original_created_before]
  108. post = Post.find(params[:id].to_i)
  109. if post.update(title:, original_created_from:, original_created_before:)
  110. tags = post.tags.where(category: 'nico').to_a +
  111. Tag.normalise_tags(tag_names, with_tagme: false)
  112. tags = Tag.expand_parent_tags(tags)
  113. sync_post_tags!(post, tags)
  114. post.reload
  115. json = post.as_json
  116. json['tags'] = build_tag_tree_for(post.tags)
  117. render json:, status: :ok
  118. else
  119. render json: post.errors, status: :unprocessable_entity
  120. end
  121. rescue Tag::NicoTagNormalisationError
  122. head :bad_request
  123. end
  124. def changes
  125. id = params[:id].presence
  126. page = (params[:page].presence || 1).to_i
  127. limit = (params[:limit].presence || 20).to_i
  128. page = 1 if page < 1
  129. limit = 1 if limit < 1
  130. offset = (page - 1) * limit
  131. pts = PostTag.with_discarded
  132. pts = pts.where(post_id: id) if id.present?
  133. pts = pts.includes(:post, :created_user, :deleted_user,
  134. tag: { tag_name: :wiki_page })
  135. events = []
  136. pts.each do |pt|
  137. tag = TagRepr.base(pt.tag)
  138. post = pt.post
  139. events << Event.new(
  140. post:,
  141. tag:,
  142. user: pt.created_user && { id: pt.created_user.id, name: pt.created_user.name },
  143. change_type: 'add',
  144. timestamp: pt.created_at)
  145. if pt.discarded_at
  146. events << Event.new(
  147. post:,
  148. tag:,
  149. user: pt.deleted_user && { id: pt.deleted_user.id, name: pt.deleted_user.name },
  150. change_type: 'remove',
  151. timestamp: pt.discarded_at)
  152. end
  153. end
  154. events.sort_by!(&:timestamp)
  155. events.reverse!
  156. render json: { changes: (events.slice(offset, limit) || []).as_json, count: events.size }
  157. end
  158. private
  159. def filtered_posts
  160. tag_names = params[:tags].to_s.split
  161. match_type = params[:match]
  162. if tag_names.present?
  163. filter_posts_by_tags(tag_names, match_type)
  164. else
  165. Post.all
  166. end
  167. end
  168. def filter_posts_by_tags tag_names, match_type
  169. literals = tag_names.map do |raw_name|
  170. { name: TagName.canonicalise(raw_name.sub(/\Anot:/i, '')).first,
  171. negative: raw_name.downcase.start_with?('not:') }
  172. end
  173. return Post.all if literals.empty?
  174. if match_type == 'any'
  175. literals.reduce(Post.none) do |posts, literal|
  176. posts.or(tag_literal_relation(literal[:name], negative: literal[:negative]))
  177. end
  178. else
  179. literals.reduce(Post.all) do |posts, literal|
  180. ids = tagged_post_ids_for(literal[:name])
  181. if literal[:negative]
  182. posts.where.not(id: ids)
  183. else
  184. posts.where(id: ids)
  185. end
  186. end
  187. end
  188. end
  189. def tag_literal_relation name, negative:
  190. ids = tagged_post_ids_for(name)
  191. if negative
  192. Post.where.not(id: ids)
  193. else
  194. Post.where(id: ids)
  195. end
  196. end
  197. def tagged_post_ids_for(name) =
  198. Post.joins(tags: :tag_name).where(tag_names: { name: }).select(:id)
  199. def sync_post_tags! post, desired_tags
  200. desired_tags.each do |t|
  201. t.save! if t.new_record?
  202. end
  203. desired_ids = desired_tags.map(&:id).to_set
  204. current_ids = post.tags.pluck(:id).to_set
  205. to_add = desired_ids - current_ids
  206. to_remove = current_ids - desired_ids
  207. Tag.where(id: to_add).find_each do |tag|
  208. begin
  209. PostTag.create!(post:, tag:, created_user: current_user)
  210. rescue ActiveRecord::RecordNotUnique
  211. ;
  212. end
  213. end
  214. PostTag.where(post_id: post.id, tag_id: to_remove.to_a).kept.find_each do |pt|
  215. pt.discard_by!(current_user)
  216. end
  217. end
  218. def build_tag_tree_for tags
  219. tags = tags.to_a
  220. tag_ids = tags.map(&:id)
  221. implications = TagImplication.where(parent_tag_id: tag_ids, tag_id: tag_ids)
  222. children_ids_by_parent = Hash.new { |h, k| h[k] = [] }
  223. implications.each do |imp|
  224. children_ids_by_parent[imp.parent_tag_id] << imp.tag_id
  225. end
  226. child_ids = children_ids_by_parent.values.flatten.uniq
  227. root_ids = tag_ids - child_ids
  228. tags_by_id = tags.index_by(&:id)
  229. memo = { }
  230. build_node = -> tag_id, path do
  231. tag = tags_by_id[tag_id]
  232. return nil unless tag
  233. if path.include?(tag_id)
  234. return TagRepr.base(tag).merge(children: [])
  235. end
  236. if memo.key?(tag_id)
  237. return memo[tag_id]
  238. end
  239. new_path = path + [tag_id]
  240. child_ids = children_ids_by_parent[tag_id] || []
  241. children = child_ids.filter_map { |cid| build_node.(cid, new_path) }
  242. memo[tag_id] = TagRepr.base(tag).merge(children:)
  243. end
  244. root_ids.filter_map { |id| build_node.call(id, []) }
  245. end
  246. end