Merge branch 'main' into feature/142

このコミットが含まれているのは:
2025-12-26 00:29:44 +09:00
コミット 32bfd5828d
20個のファイルの変更744行の追加203行の削除
+2
ファイルの表示
@@ -63,3 +63,5 @@ gem 'diff-lcs'
gem 'dotenv-rails'
gem 'whenever', require: false
gem 'discard'
+3
ファイルの表示
@@ -90,6 +90,8 @@ GEM
crass (1.0.6)
date (3.4.1)
diff-lcs (1.6.2)
discard (1.4.0)
activerecord (>= 4.2, < 9.0)
dotenv (3.1.8)
dotenv-rails (3.1.8)
dotenv (= 3.1.8)
@@ -420,6 +422,7 @@ DEPENDENCIES
bootsnap
brakeman
diff-lcs
discard
dotenv-rails
gollum
image_processing (~> 1.14)
+77 -11
ファイルの表示
@@ -1,8 +1,6 @@
require 'open-uri'
require 'nokogiri'
class PostsController < ApplicationController
Event = Struct.new(:post, :tag, :user, :change_type, :timestamp, keyword_init: true)
# GET /posts
def index
limit = params[:limit].presence&.to_i
@@ -80,8 +78,9 @@ class PostsController < ApplicationController
post.thumbnail.attach(thumbnail)
if post.save
post.resized_thumbnail!
post.tags = Tag.normalise_tags(tag_names)
post.tags = Tag.expand_parent_tags(post.tags)
tags = Tag.normalise_tags(tag_names)
tags = Tag.expand_parent_tags(tags)
sync_post_tags!(post, tags)
render json: post.as_json(include: { tags: { only: [:id, :name, :category, :post_count] } }),
status: :created
else
@@ -114,10 +113,11 @@ class PostsController < ApplicationController
original_created_before = params[:original_created_before]
post = Post.find(params[:id].to_i)
tags = post.tags.where(category: 'nico').to_a +
Tag.normalise_tags(tag_names, with_tagme: false)
tags = Tag.expand_parent_tags(tags)
if post.update(title:, tags:, original_created_from:, original_created_before:)
if post.update(title:, original_created_from:, original_created_before:)
tags = post.tags.where(category: 'nico').to_a +
Tag.normalise_tags(tag_names, with_tagme: false)
tags = Tag.expand_parent_tags(tags)
sync_post_tags!(post, tags)
json = post.as_json
json['tags'] = build_tag_tree_for(post.tags)
render json:, status: :ok
@@ -130,12 +130,54 @@ class PostsController < ApplicationController
def destroy
end
def changes
id = params[:id]
page = (params[:page].presence || 1).to_i
limit = (params[:limit].presence || 20).to_i
page = 1 if page < 1
limit = 1 if limit < 1
offset = (page - 1) * limit
pts = PostTag.with_discarded
pts = pts.where(post_id: id) if id.present?
pts = pts.includes(:post, :tag, :created_user, :deleted_user)
events = []
pts.each do |pt|
events << Event.new(
post: pt.post,
tag: pt.tag,
user: pt.created_user && { id: pt.created_user.id, name: pt.created_user.name },
change_type: 'add',
timestamp: pt.created_at)
if pt.discarded_at
events << Event.new(
post: pt.post,
tag: pt.tag,
user: pt.deleted_user && { id: pt.deleted_user.id, name: pt.deleted_user.name },
change_type: 'remove',
timestamp: pt.discarded_at)
end
end
events.sort_by!(&:timestamp)
events.reverse!
render json: { changes: events.slice(offset, limit).as_json, count: events.size }
end
private
def filtered_posts
tag_names = params[:tags]&.split(' ')
match_type = params[:match]
tag_names.present? ? filter_posts_by_tags(tag_names, match_type) : Post.all
if tag_names.present?
filter_posts_by_tags(tag_names, match_type)
else
Post.all
end
end
def filter_posts_by_tags tag_names, match_type
@@ -150,6 +192,30 @@ class PostsController < ApplicationController
posts.distinct
end
def sync_post_tags! post, desired_tags
desired_tags.each do |t|
t.save! if t.new_record?
end
desired_ids = desired_tags.map(&:id).to_set
current_ids = post.tags.pluck(:id).to_set
to_add = desired_ids - current_ids
to_remove = current_ids - desired_ids
Tag.where(id: to_add).find_each do |tag|
begin
PostTag.create!(post:, tag:, created_user: current_user)
rescue ActiveRecord::RecordNotUnique
;
end
end
PostTag.where(post_id: post.id, tag_id: to_remove.to_a).kept.find_each do |pt|
pt.discard_by!(current_user)
end
end
def build_tag_tree_for tags
tags = tags.to_a
tag_ids = tags.map(&:id)
+7 -5
ファイルの表示
@@ -1,11 +1,13 @@
require 'mini_magick'
class Post < ApplicationRecord
require 'mini_magick'
belongs_to :parent, class_name: 'Post', optional: true, foreign_key: 'parent_id'
belongs_to :uploaded_user, class_name: 'User', optional: true
has_many :post_tags, dependent: :destroy
has_many :tags, through: :post_tags
has_many :post_tags, dependent: :destroy, inverse_of: :post
has_many :active_post_tags, -> { kept }, class_name: 'PostTag', inverse_of: :post
has_many :post_tags_with_discarded, -> { with_discarded }, class_name: 'PostTag'
has_many :tags, through: :active_post_tags
has_many :user_post_views, dependent: :destroy
has_many :post_similarities_as_post,
class_name: 'PostSimilarity',
+18
ファイルの表示
@@ -1,7 +1,25 @@
class PostTag < ApplicationRecord
include Discard::Model
belongs_to :post
belongs_to :tag, counter_cache: :post_count
belongs_to :created_user, class_name: 'User', optional: true
belongs_to :deleted_user, class_name: 'User', optional: true
validates :post_id, presence: true
validates :tag_id, presence: true
validates :post_id, uniqueness: {
scope: :tag_id,
conditions: -> { where(discarded_at: nil) } }
def discard_by! deleted_user
return self if discarded?
transaction do
update!(discarded_at: Time.current, deleted_user:)
Tag.where(id: tag_id).update_all('post_count = GREATEST(post_count - 1, 0)')
end
self
end
end
+6 -4
ファイルの表示
@@ -1,6 +1,8 @@
class Tag < ApplicationRecord
has_many :post_tags, dependent: :destroy
has_many :posts, through: :post_tags
has_many :post_tags, dependent: :delete_all, inverse_of: :tag
has_many :active_post_tags, -> { kept }, class_name: 'PostTag', inverse_of: :tag
has_many :post_tags_with_discarded, -> { with_discarded }, class_name: 'PostTag'
has_many :posts, through: :active_post_tags
has_many :tag_aliases, dependent: :destroy
has_many :nico_tag_relations, foreign_key: :nico_tag_id, dependent: :destroy
@@ -43,13 +45,13 @@ class Tag < ApplicationRecord
'meta:' => 'meta' }.freeze
def self.tagme
@tagme ||= Tag.find_or_initialize_by(name: 'タグ希望') do |tag|
@tagme ||= Tag.find_or_create_by!(name: 'タグ希望') do |tag|
tag.category = 'meta'
end
end
def self.bot
@bot ||= Tag.find_or_initialize_by(name: 'bot操作') do |tag|
@bot ||= Tag.find_or_create_by!(name: 'bot操作') do |tag|
tag.category = 'meta'
end
end
+1
ファイルの表示
@@ -4,6 +4,7 @@ Rails.application.routes.draw do
get 'tags/autocomplete', to: 'tags#autocomplete'
get 'tags/name/:name', to: 'tags#show_by_name'
get 'posts/random', to: 'posts#random'
get 'posts/changes', to: 'posts#changes'
post 'posts/:id/viewed', to: 'posts#viewed'
delete 'posts/:id/viewed', to: 'posts#unviewed'
get 'preview/title', to: 'preview#title'
+36
ファイルの表示
@@ -0,0 +1,36 @@
class AddDiscardToPostTags < ActiveRecord::Migration[8.0]
def up
execute <<~SQL
DELETE
pt1
FROM
post_tags pt1
INNER JOIN
post_tags pt2
ON
pt1.post_id = pt2.post_id
AND pt1.tag_id = pt2.tag_id
AND pt1.id > pt2.id
;
SQL
add_column :post_tags, :discarded_at, :datetime
add_index :post_tags, :discarded_at
add_column :post_tags, :is_active, :boolean,
as: 'discarded_at IS NULL', stored: true
add_column :post_tags, :active_unique_key, :string,
as: "CASE WHEN discarded_at IS NULL THEN CONCAT(post_id, ':', tag_id) ELSE NULL END",
stored: true
add_index :post_tags, :active_unique_key, unique: true, name: 'idx_post_tags_active_unique'
add_index :post_tags, [:post_id, :discarded_at]
add_index :post_tags, [:tag_id, :discarded_at]
end
def down
raise ActiveRecord::IrreversibleMigration, '戻せません.'
end
end
+70 -36
ファイルの表示
@@ -5,12 +5,32 @@ namespace :nico do
require 'open-uri'
require 'nokogiri'
fetch_thumbnail = -> url {
fetch_thumbnail = -> url do
html = URI.open(url, read_timeout: 60, 'User-Agent' => 'Mozilla/5.0').read
doc = Nokogiri::HTML(html)
doc.at('meta[name="thumbnail"]')&.[]('content').presence
}
end
def sync_post_tags! post, desired_tag_ids
desired_ids = desired_tag_ids.compact.to_set
current_ids = post.tags.pluck(:id).to_set
to_add = desired_ids - current_ids
to_remove = current_ids - desired_ids
Tag.where(id: to_add.to_a).find_each do |tag|
begin
PostTag.create!(post:, tag:)
rescue ActiveRecord::RecordNotUnique
;
end
end
PostTag.where(post_id: post.id, tag_id: to_remove.to_a).kept.find_each do |pt|
pt.discard_by!(nil)
end
end
mysql_user = ENV['MYSQL_USER']
mysql_pass = ENV['MYSQL_PASS']
@@ -19,43 +39,57 @@ namespace :nico do
{ 'MYSQL_USER' => mysql_user, 'MYSQL_PASS' => mysql_pass },
'python3', "#{ nizika_nico_path }/get_videos.py")
if status.success?
data = JSON.parse(stdout)
data.each do |datum|
post = Post.where('url LIKE ?', '%nicovideo.jp%').find { |post|
post.url =~ %r{#{ Regexp.escape(datum['code']) }(?!\d)}
}
unless post
title = datum['title']
url = "https://www.nicovideo.jp/watch/#{ datum['code'] }"
thumbnail_base = fetch_thumbnail.(url) || '' rescue ''
post = Post.new(title:, url:, thumbnail_base:, uploaded_user: nil)
if thumbnail_base.present?
post.thumbnail.attach(
io: URI.open(thumbnail_base),
filename: File.basename(URI.parse(thumbnail_base).path),
content_type: 'image/jpeg')
end
post.save!
post.resized_thumbnail!
end
abort unless status.success?
current_tags = post.tags.where(category: 'nico').pluck(:name).sort
new_tags = datum['tags'].map { |tag| "nico:#{ tag }" }.sort
if current_tags != new_tags
post.tags.destroy(post.tags.where(name: current_tags))
tags_to_add = []
new_tags.each do |name|
tag = Tag.find_or_initialize_by(name:) do |t|
t.category = 'nico'
end
tags_to_add.concat([tag] + tag.linked_tags)
end
tags_to_add << Tag.tagme if post.tags.size < 10
tags_to_add << Tag.bot
post.tags = (post.tags + tags_to_add).uniq
data = JSON.parse(stdout)
data.each do |datum|
post = Post.where('url LIKE ?', '%nicovideo.jp%').find { |post|
post.url =~ %r{#{ Regexp.escape(datum['code']) }(?!\d)}
}
unless post
title = datum['title']
url = "https://www.nicovideo.jp/watch/#{ datum['code'] }"
thumbnail_base = fetch_thumbnail.(url) || '' rescue ''
post = Post.new(title:, url:, thumbnail_base:, uploaded_user: nil)
if thumbnail_base.present?
post.thumbnail.attach(
io: URI.open(thumbnail_base),
filename: File.basename(URI.parse(thumbnail_base).path),
content_type: 'image/jpeg')
end
post.save!
post.resized_thumbnail!
sync_post_tags!(post, [Tag.tagme.id])
end
kept_tags = post.tags.reload
kept_non_nico_ids = kept_tags.where.not(category: 'nico').pluck(:id).to_set
desired_nico_ids = []
desired_non_nico_ids = []
datum['tags'].each do |raw|
name = "nico:#{ raw }"
tag = Tag.find_or_initialize_by(name:) do |t|
t.category = 'nico'
end
tag.save! if tag.new_record?
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))
end
end
desired_nico_ids.uniq!
desired_all_ids = kept_non_nico_ids.to_a + desired_nico_ids
desired_non_nico_ids.concat(kept_non_nico_ids.to_a)
desired_non_nico_ids.uniq!
if kept_non_nico_ids.to_set != desired_non_nico_ids.to_set
desired_all_ids << Tag.bot.id
end
desired_all_ids.uniq!
sync_post_tags!(post, desired_all_ids)
end
end
end