Browse Source

#215

pull/219/head
みてるぞ 1 month ago
parent
commit
5145db250d
8 changed files with 108 additions and 65 deletions
  1. +20
    -19
      backend/app/controllers/posts_controller.rb
  2. +17
    -11
      backend/app/controllers/tags_controller.rb
  3. +12
    -7
      backend/app/controllers/wiki_pages_controller.rb
  4. +4
    -0
      backend/app/models/post_tag.rb
  5. +21
    -11
      backend/app/models/tag.rb
  6. +9
    -0
      backend/app/models/tag_name.rb
  7. +10
    -1
      backend/app/models/wiki_page.rb
  8. +15
    -16
      backend/lib/tasks/sync_nico.rake

+ 20
- 19
backend/app/controllers/posts_controller.rb View File

@@ -1,7 +1,6 @@
class PostsController < ApplicationController class PostsController < ApplicationController
Event = Struct.new(:post, :tag, :user, :change_type, :timestamp, keyword_init: true) Event = Struct.new(:post, :tag, :user, :change_type, :timestamp, keyword_init: true)


# GET /posts
def index def index
page = (params[:page].presence || 1).to_i page = (params[:page].presence || 1).to_i
limit = (params[:limit].presence || 20).to_i limit = (params[:limit].presence || 20).to_i
@@ -18,7 +17,7 @@ class PostsController < ApplicationController
'posts.created_at)' 'posts.created_at)'
q = q =
filtered_posts filtered_posts
.preload(:tags)
.preload(tags: :tag_name)
.with_attached_thumbnail .with_attached_thumbnail
.select("posts.*, #{ sort_sql } AS sort_ts") .select("posts.*, #{ sort_sql } AS sort_ts")
.order(Arel.sql("#{ sort_sql } DESC")) .order(Arel.sql("#{ sort_sql } DESC"))
@@ -36,7 +35,8 @@ class PostsController < ApplicationController
end end


render json: { posts: posts.map { |post| render json: { posts: posts.map { |post|
post.as_json(include: { tags: { only: [:id, :name, :category, :post_count] } }).tap do |json|
post.as_json(include: { tags: { only: [:id, :category, :post_count],
methods: [:name] } }).tap do |json|
json['thumbnail'] = json['thumbnail'] =
if post.thumbnail.attached? if post.thumbnail.attached?
rails_storage_proxy_url(post.thumbnail, only_path: false) rails_storage_proxy_url(post.thumbnail, only_path: false)
@@ -48,19 +48,19 @@ class PostsController < ApplicationController
end end


def random def random
post = filtered_posts.order('RAND()').first
post = filtered_posts.preload(tags: :tag_name).order('RAND()').first
return head :not_found unless post return head :not_found unless post


viewed = current_user&.viewed?(post) || false viewed = current_user&.viewed?(post) || false


render json: (post render json: (post
.as_json(include: { tags: { only: [:id, :name, :category, :post_count] } })
.as_json(include: { tags: { only: [:id, :category, :post_count],
methods: [:name] } })
.merge(viewed:)) .merge(viewed:))
end end


# GET /posts/1
def show def show
post = Post.includes(:tags).find(params[:id])
post = Post.includes(tags: :tag_name).find(params[:id])
return head :not_found unless post return head :not_found unless post


viewed = current_user&.viewed?(post) || false viewed = current_user&.viewed?(post) || false
@@ -73,7 +73,6 @@ class PostsController < ApplicationController
render json: render json:
end end


# POST /posts
def create def create
return head :unauthorized unless current_user return head :unauthorized unless current_user
return head :forbidden unless current_user.member? return head :forbidden unless current_user.member?
@@ -96,7 +95,8 @@ class PostsController < ApplicationController
tags = Tag.normalise_tags(tag_names) tags = Tag.normalise_tags(tag_names)
tags = Tag.expand_parent_tags(tags) tags = Tag.expand_parent_tags(tags)
sync_post_tags!(post, tags) sync_post_tags!(post, tags)
render json: post.as_json(include: { tags: { only: [:id, :name, :category, :post_count] } }),
render json: post.as_json(include: { tags: { only: [:id, :category, :post_count],
methods: [:name] } }),
status: :created status: :created
else else
render json: { errors: post.errors.full_messages }, status: :unprocessable_entity render json: { errors: post.errors.full_messages }, status: :unprocessable_entity
@@ -117,7 +117,6 @@ class PostsController < ApplicationController
head :no_content head :no_content
end end


# PATCH/PUT /posts/1
def update def update
return head :unauthorized unless current_user return head :unauthorized unless current_user
return head :forbidden unless current_user.member? return head :forbidden unless current_user.member?
@@ -153,7 +152,7 @@ class PostsController < ApplicationController


pts = PostTag.with_discarded pts = PostTag.with_discarded
pts = pts.where(post_id: id) if id.present? pts = pts.where(post_id: id) if id.present?
pts = pts.includes(:post, :tag, :created_user, :deleted_user)
pts = pts.includes(:post, { tag: :tag_name }, :created_user, :deleted_user)


events = [] events = []
pts.each do |pt| pts.each do |pt|
@@ -192,15 +191,15 @@ class PostsController < ApplicationController
end end


def filter_posts_by_tags tag_names, match_type def filter_posts_by_tags tag_names, match_type
posts = Post.joins(:tags)
posts = Post.joins(tags: :tag_name)

if match_type == 'any' if match_type == 'any'
posts = posts.where(tags: { name: tag_names }).distinct
posts.where(tag_names: { name: tag_names }).distinct
else else
tag_names.each do |tag|
posts = posts.where(id: Post.joins(:tags).where(tags: { name: tag }))
end
posts.where(tag_names: { name: tag_names })
.group('posts.id')
.having('COUNT(DISTINCT tag_names.id) = ?', tag_names.uniq.size)
end end
posts.distinct
end end


def sync_post_tags! post, desired_tags def sync_post_tags! post, desired_tags
@@ -251,7 +250,8 @@ class PostsController < ApplicationController
return nil unless tag return nil unless tag


if path.include?(tag_id) if path.include?(tag_id)
return tag.as_json(only: [:id, :name, :category, :post_count]).merge(children: [])
return tag.as_json(only: [:id, :category, :post_count],
methods: [:name]).merge(children: [])
end end


if memo.key?(tag_id) if memo.key?(tag_id)
@@ -263,7 +263,8 @@ class PostsController < ApplicationController


children = child_ids.filter_map { |cid| build_node.(cid, new_path) } children = child_ids.filter_map { |cid| build_node.(cid, new_path) }


memo[tag_id] = tag.as_json(only: [:id, :name, :category, :post_count]).merge(children:)
memo[tag_id] = tag.as_json(only: [:id, :category, :post_count],
methods: [:name]).merge(children:)
end end


root_ids.filter_map { |id| build_node.call(id, []) } root_ids.filter_map { |id| build_node.call(id, []) }


+ 17
- 11
backend/app/controllers/tags_controller.rb View File

@@ -1,33 +1,39 @@
class TagsController < ApplicationController class TagsController < ApplicationController
def index def index
post_id = params[:post] post_id = params[:post]
tags = if post_id.present?
Tag.joins(:posts).where(posts: { id: post_id })
else
Tag.all
end
render json: tags

tags =
if post_id.present?
Tag.joins(:posts).where(posts: { id: post_id })
else
Tag.all
end

render json: tags.includes(:tag_name)
end end


def autocomplete def autocomplete
q = params[:q].to_s.strip q = params[:q].to_s.strip
return render json: [] if q.blank? return render json: [] if q.blank?


tags = (Tag
.where('(category = ? AND name LIKE ?) OR name LIKE ?',
tags = (Tag.joins(:tag_name).includes(:tag_name)
.where('(tags.category = ? AND tag_names.name LIKE ?) OR tag_names.name LIKE ?',
'nico', "nico:#{ q }%", "#{ q }%") 'nico', "nico:#{ q }%", "#{ q }%")
.order('post_count DESC, name ASC')
.order(Arel.sql('post_count DESC, tag_names.name ASC'))
.limit(20)) .limit(20))
render json: tags render json: tags
end end


def show def show
tag = Tag.find(params[:id])
tag = Tag.find_by(id: params[:id])
render json: tag render json: tag
end end


def show_by_name def show_by_name
tag = Tag.find_by(name: params[:name])
name = params[:name].to_s.strip
return head :bad_request if name.blank?

tag = Tag.joins(:tag_name).includes(:tag_name).find_by(tag_names: { name: })
if tag if tag
render json: tag render json: tag
else else


+ 12
- 7
backend/app/controllers/wiki_pages_controller.rb View File

@@ -6,15 +6,19 @@ class WikiPagesController < ApplicationController
end end


def show def show
render_wiki_page_or_404 WikiPage.find(params[:id])
render_wiki_page_or_404 WikiPage.includes(:tag_name).find_by(id: params[:id])
end end


def show_by_title def show_by_title
render_wiki_page_or_404 WikiPage.find_by(title: params[:title])
title = params[:title].to_s.strip
page = WikiPage.joins(:tag_name)
.includes(:tag_name)
.find_by(tag_names: { name: title })
render_wiki_page_or_404 page
end end


def exists def exists
if WikiPage.exists?(params[:id])
if WikiPage.exists?(id: params[:id])
head :no_content head :no_content
else else
head :not_found head :not_found
@@ -22,7 +26,8 @@ class WikiPagesController < ApplicationController
end end


def exists_by_title def exists_by_title
if WikiPage.exists?(title: params[:title])
title = params[:title].to_s.strip
if WikiPage.joins(:tag_name).exists?(tag_names: { name: title })
head :no_content head :no_content
else else
head :not_found head :not_found
@@ -115,11 +120,11 @@ class WikiPagesController < ApplicationController
end end


def search def search
title = params[:title]&.strip
title = params[:title].to_s.strip


q = WikiPage.all
q = WikiPage.joins(:tag_name).includes(:tag_name)
if title.present? if title.present?
q = q.where('title LIKE ?', "%#{ WikiPage.sanitize_sql_like(title) }%")
q = q.where('tag_names.name LIKE ?', "%#{ WikiPage.sanitize_sql_like(title) }%")
end end


render json: q.limit(20) render json: q.limit(20)


+ 4
- 0
backend/app/models/post_tag.rb View File

@@ -1,6 +1,10 @@
class PostTag < ApplicationRecord class PostTag < ApplicationRecord
include Discard::Model include Discard::Model


before_destroy do
raise ActiveRecord::ReadOnlyRecord, '消さないでください.'
end

belongs_to :post belongs_to :post
belongs_to :tag, counter_cache: :post_count belongs_to :tag, counter_cache: :post_count
belongs_to :created_user, class_name: 'User', optional: true belongs_to :created_user, class_name: 'User', optional: true


+ 21
- 11
backend/app/models/tag.rb View File

@@ -21,6 +21,10 @@ class Tag < ApplicationRecord
dependent: :destroy dependent: :destroy
has_many :parents, through: :reversed_tag_implications, source: :parent_tag has_many :parents, through: :reversed_tag_implications, source: :parent_tag


belongs_to :tag_name
delegate :name, to: :tag_name
validates :tag_name, presence: true

enum :category, { deerjikist: 'deerjikist', enum :category, { deerjikist: 'deerjikist',
meme: 'meme', meme: 'meme',
character: 'character', character: 'character',
@@ -29,7 +33,6 @@ class Tag < ApplicationRecord
nico: 'nico', nico: 'nico',
meta: 'meta' } meta: 'meta' }


validates :name, presence: true, length: { maximum: 255 }
validates :category, presence: true, inclusion: { in: Tag.categories.keys } validates :category, presence: true, inclusion: { in: Tag.categories.keys }


validate :nico_tag_name_must_start_with_nico validate :nico_tag_name_must_start_with_nico
@@ -44,31 +47,31 @@ class Tag < ApplicationRecord
'mtr:' => 'material', 'mtr:' => 'material',
'meta:' => 'meta' }.freeze 'meta:' => 'meta' }.freeze


def name= val
(self.tag_name ||= build_tag_name).name = val
end

def self.tagme def self.tagme
@tagme ||= Tag.find_or_create_by!(name: 'タグ希望') do |tag|
tag.category = 'meta'
end
@tagme ||= find_or_create_by_tag_name!('タグ希望', category: 'meta')
end end


def self.bot def self.bot
@bot ||= Tag.find_or_create_by!(name: 'bot操作') do |tag|
tag.category = 'meta'
end
@bot ||= find_or_create_by_tag_name!('bot操作', category: 'meta')
end end


def self.normalise_tags tag_names, with_tagme: true def self.normalise_tags tag_names, with_tagme: true
tags = tag_names.map do |name| tags = tag_names.map do |name|
pf, cat = CATEGORY_PREFIXES.find { |p, _| name.start_with?(p) } || ['', nil] pf, cat = CATEGORY_PREFIXES.find { |p, _| name.start_with?(p) } || ['', nil]
name.delete_prefix!(pf) name.delete_prefix!(pf)
Tag.find_or_initialize_by(name:).tap do |tag|
find_or_create_by_tag_name!(name, category: (cat || 'general')).tap do |tag|
if cat && tag.category != cat if cat && tag.category != cat
tag.category = cat
tag.save!
tag.update!(category: cat)
end end
end end
end end

tags << Tag.tagme if with_tagme && tags.size < 10 && tags.none?(Tag.tagme) tags << Tag.tagme if with_tagme && tags.size < 10 && tags.none?(Tag.tagme)
tags.uniq
tags.uniq(&:id)
end end


def self.expand_parent_tags tags def self.expand_parent_tags tags
@@ -94,6 +97,13 @@ class Tag < ApplicationRecord
(result + tags).uniq { |t| t.id } (result + tags).uniq { |t| t.id }
end end


def self.find_or_create_by_tag_name!(name, category:)
tn = TagName.find_or_create_by!(name: name.to_s.strip)
Tag.find_or_create_by!(tag_name_id: tn.id) do |t|
t.category = category
end
end

private private


def nico_tag_name_must_start_with_nico def nico_tag_name_must_start_with_nico


+ 9
- 0
backend/app/models/tag_name.rb View File

@@ -0,0 +1,9 @@
class TagName < ApplicationRecord
has_one :tag
has_one :wiki_page

belongs_to :canonical, class_name: 'TagName', optional: true
has_many :aliases, class_name: 'TagName', foreign_key: :canonical_id

validates :name, presence: true, length: { maximum: 255 }, uniqueness: true
end

+ 10
- 1
backend/app/models/wiki_page.rb View File

@@ -11,7 +11,16 @@ class WikiPage < ApplicationRecord
foreign_key: :redirect_page_id, foreign_key: :redirect_page_id,
dependent: :nullify dependent: :nullify


validates :title, presence: true, length: { maximum: 255 }, uniqueness: true
belongs_to :tag_name
validates :tag_name, presence: true

def title
tag_name.name
end

def title= val
(self.tag_name ||= build_tag_name).name = val
end


def current_revision def current_revision
wiki_revisions.order(id: :desc).first wiki_revisions.order(id: :desc).first


+ 15
- 16
backend/lib/tasks/sync_nico.rake View File

@@ -4,6 +4,7 @@ namespace :nico do
require 'open3' require 'open3'
require 'open-uri' require 'open-uri'
require 'nokogiri' require 'nokogiri'
require 'set'


fetch_thumbnail = -> url do fetch_thumbnail = -> url do
html = URI.open(url, read_timeout: 60, 'User-Agent' => 'Mozilla/5.0').read html = URI.open(url, read_timeout: 60, 'User-Agent' => 'Mozilla/5.0').read
@@ -12,9 +13,9 @@ namespace :nico do
doc.at('meta[name="thumbnail"]')&.[]('content').presence doc.at('meta[name="thumbnail"]')&.[]('content').presence
end end


def sync_post_tags! post, desired_tag_ids
def sync_post_tags! post, desired_tag_ids, current_ids: nil
current_ids ||= PostTag.kept.where(post_id: post.id).pluck(:tag_id).to_set
desired_ids = desired_tag_ids.compact.to_set desired_ids = desired_tag_ids.compact.to_set
current_ids = post.tags.pluck(:id).to_set


to_add = desired_ids - current_ids to_add = desired_ids - current_ids
to_remove = current_ids - desired_ids to_remove = current_ids - desired_ids
@@ -43,12 +44,12 @@ namespace :nico do


data = JSON.parse(stdout) data = JSON.parse(stdout)
data.each do |datum| data.each do |datum|
post = Post.where('url LIKE ?', '%nicovideo.jp%').find { |post|
post.url =~ %r{#{ Regexp.escape(datum['code']) }(?!\d)}
}
code = datum['code']
post = Post.where('url REGEXP ?', "nicovideo\\.jp/watch/#{ Regexp.escape(code) }([^0-9]|$)")
.first
unless post unless post
title = datum['title'] title = datum['title']
url = "https://www.nicovideo.jp/watch/#{ datum['code'] }"
url = "https://www.nicovideo.jp/watch/#{ code }"
thumbnail_base = fetch_thumbnail.(url) rescue nil thumbnail_base = fetch_thumbnail.(url) rescue nil
post = Post.new(title:, url:, thumbnail_base:, uploaded_user: nil) post = Post.new(title:, url:, thumbnail_base:, uploaded_user: nil)
if thumbnail_base.present? if thumbnail_base.present?
@@ -62,21 +63,19 @@ namespace :nico do
sync_post_tags!(post, [Tag.tagme.id]) sync_post_tags!(post, [Tag.tagme.id])
end end


kept_tags = post.tags.reload
kept_non_nico_ids = kept_tags.where.not(category: 'nico').pluck(:id).to_set
kept_ids = PostTag.kept.where(post_id: post.id).pluck(:tag_id).to_set
kept_non_nico_ids = post.tags.where.not(category: 'nico').pluck(:id).to_set


desired_nico_ids = [] desired_nico_ids = []
desired_non_nico_ids = [] desired_non_nico_ids = []
datum['tags'].each do |raw| datum['tags'].each do |raw|
name = "nico:#{ raw }" name = "nico:#{ raw }"
tag = Tag.find_or_initialize_by(name:) do |t|
t.category = 'nico'
end
tag.save! if tag.new_record?
tag = Tag.find_or_create_by_tag_name!(name, category: 'nico')
desired_nico_ids << tag.id desired_nico_ids << tag.id
unless tag.in?(kept_tags)
desired_non_nico_ids.concat(tag.linked_tags.pluck(:id))
desired_nico_ids.concat(tag.linked_tags.pluck(:id))
unless tag.id.in?(kept_ids)
linked_ids = tag.linked_tags.pluck(:id)
desired_non_nico_ids.concat(linked_ids)
desired_nico_ids.concat(linked_ids)
end end
end end
desired_nico_ids.uniq! desired_nico_ids.uniq!
@@ -89,7 +88,7 @@ namespace :nico do
end end
desired_all_ids.uniq! desired_all_ids.uniq!


sync_post_tags!(post, desired_all_ids)
sync_post_tags!(post, desired_all_ids, current_ids: kept_ids)
end end
end end
end end

Loading…
Cancel
Save