Merge remote-tracking branch 'origin/main' into #92
This commit is contained in:
@@ -63,3 +63,5 @@ gem 'diff-lcs'
|
||||
gem 'dotenv-rails'
|
||||
|
||||
gem 'whenever', require: false
|
||||
|
||||
gem 'discard'
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,34 +1,50 @@
|
||||
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
|
||||
page = (params[:page].presence || 1).to_i
|
||||
limit = (params[:limit].presence || 20).to_i
|
||||
cursor = params[:cursor].presence
|
||||
|
||||
q = filtered_posts.order(created_at: :desc)
|
||||
q = q.where('posts.created_at < ?', Time.iso8601(cursor)) if cursor
|
||||
page = 1 if page < 1
|
||||
limit = 1 if limit < 1
|
||||
|
||||
posts = limit ? q.limit(limit + 1) : q
|
||||
offset = (page - 1) * limit
|
||||
|
||||
sort_sql =
|
||||
'COALESCE(posts.original_created_before - INTERVAL 1 SECOND,' +
|
||||
'posts.original_created_from,' +
|
||||
'posts.created_at)'
|
||||
q =
|
||||
filtered_posts
|
||||
.preload(:tags)
|
||||
.with_attached_thumbnail
|
||||
.select("posts.*, #{ sort_sql } AS sort_ts")
|
||||
.order(Arel.sql("#{ sort_sql } DESC"))
|
||||
posts = (
|
||||
if cursor
|
||||
q.where("#{ sort_sql } < ?", Time.iso8601(cursor)).limit(limit + 1)
|
||||
else
|
||||
q.limit(limit).offset(offset)
|
||||
end).to_a
|
||||
|
||||
next_cursor = nil
|
||||
if limit && posts.size > limit
|
||||
next_cursor = posts.last.created_at.iso8601(6)
|
||||
if cursor && posts.length > limit
|
||||
next_cursor = posts.last.read_attribute('sort_ts').iso8601(6)
|
||||
posts = posts.first(limit)
|
||||
end
|
||||
|
||||
render json: { posts: posts.map { |post|
|
||||
post.as_json(include: { tags: { only: [:id, :name, :category, :post_count] } }).tap { |json|
|
||||
post.as_json(include: { tags: { only: [:id, :name, :category, :post_count] } }).tap do |json|
|
||||
json['thumbnail'] =
|
||||
if post.thumbnail.attached?
|
||||
rails_storage_proxy_url(post.thumbnail, only_path: false)
|
||||
else
|
||||
nil
|
||||
end
|
||||
}
|
||||
}, next_cursor: }
|
||||
end
|
||||
}, count: filtered_posts.count(:id), next_cursor: }
|
||||
end
|
||||
|
||||
def random
|
||||
@@ -39,7 +55,7 @@ class PostsController < ApplicationController
|
||||
|
||||
render json: (post
|
||||
.as_json(include: { tags: { only: [:id, :name, :category, :post_count] } })
|
||||
.merge(viewed: viewed))
|
||||
.merge(viewed:))
|
||||
end
|
||||
|
||||
# GET /posts/1
|
||||
@@ -49,9 +65,12 @@ class PostsController < ApplicationController
|
||||
|
||||
viewed = current_user&.viewed?(post) || false
|
||||
|
||||
render json: (post
|
||||
.as_json(include: { tags: { only: [:id, :name, :category, :post_count] } })
|
||||
.merge(related: post.related(limit: 20), viewed:))
|
||||
json = post.as_json
|
||||
json['tags'] = build_tag_tree_for(post.tags)
|
||||
json['related'] = post.related(limit: 20)
|
||||
json['viewed'] = viewed
|
||||
|
||||
render json:
|
||||
end
|
||||
|
||||
# POST /posts
|
||||
@@ -60,18 +79,23 @@ class PostsController < ApplicationController
|
||||
return head :forbidden unless current_user.member?
|
||||
|
||||
# TODO: URL が正規のものがチェック,不正ならエラー
|
||||
# TODO: title、URL は必須にする.
|
||||
# TODO: URL は必須にする(タイトルは省略可).
|
||||
# TODO: サイトに応じて thumbnail_base 設定
|
||||
title = params[:title]
|
||||
url = params[:url]
|
||||
thumbnail = params[:thumbnail]
|
||||
tag_names = params[:tags].to_s.split(' ')
|
||||
original_created_from = params[:original_created_from]
|
||||
original_created_before = params[:original_created_before]
|
||||
|
||||
post = Post.new(title:, url:, thumbnail_base: '', uploaded_user: current_user)
|
||||
post = Post.new(title:, url:, thumbnail_base: '', uploaded_user: current_user,
|
||||
original_created_from:, original_created_before:)
|
||||
post.thumbnail.attach(thumbnail)
|
||||
if post.save
|
||||
post.resized_thumbnail!
|
||||
post.tags = Tag.normalise_tags(tags_names)
|
||||
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
|
||||
@@ -100,12 +124,18 @@ class PostsController < ApplicationController
|
||||
|
||||
title = params[:title]
|
||||
tag_names = params[:tags].to_s.split(' ')
|
||||
original_created_from = params[:original_created_from]
|
||||
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)
|
||||
if post.update(title:, tags:)
|
||||
render json: post.as_json(include: { tags: { only: [:id, :name, :category, :post_count] } }),
|
||||
status: :ok
|
||||
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
|
||||
else
|
||||
render json: post.errors, status: :unprocessable_entity
|
||||
end
|
||||
@@ -115,12 +145,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
|
||||
@@ -134,4 +206,70 @@ class PostsController < ApplicationController
|
||||
end
|
||||
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)
|
||||
|
||||
implications = TagImplication.where(parent_tag_id: tag_ids, tag_id: tag_ids)
|
||||
|
||||
children_ids_by_parent = Hash.new { |h, k| h[k] = [] }
|
||||
implications.each do |imp|
|
||||
children_ids_by_parent[imp.parent_tag_id] << imp.tag_id
|
||||
end
|
||||
|
||||
child_ids = children_ids_by_parent.values.flatten.uniq
|
||||
|
||||
root_ids = tag_ids - child_ids
|
||||
|
||||
tags_by_id = tags.index_by(&:id)
|
||||
|
||||
memo = { }
|
||||
|
||||
build_node = -> tag_id, path do
|
||||
tag = tags_by_id[tag_id]
|
||||
return nil unless tag
|
||||
|
||||
if path.include?(tag_id)
|
||||
return tag.as_json(only: [:id, :name, :category, :post_count]).merge(children: [])
|
||||
end
|
||||
|
||||
if memo.key?(tag_id)
|
||||
return memo[tag_id]
|
||||
end
|
||||
|
||||
new_path = path + [tag_id]
|
||||
child_ids = children_ids_by_parent[tag_id] || []
|
||||
|
||||
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:)
|
||||
end
|
||||
|
||||
root_ids.filter_map { |id| build_node.call(id, []) }
|
||||
end
|
||||
end
|
||||
|
||||
@@ -6,12 +6,15 @@ class UsersController < ApplicationController
|
||||
end
|
||||
|
||||
def verify
|
||||
ip_bin = IPAddr.new(request.remote_ip).hton
|
||||
ip_address = IpAddress.find_or_create_by!(ip_address: ip_bin)
|
||||
|
||||
user = User.find_by(inheritance_code: params[:code])
|
||||
render json: if user
|
||||
{ valid: true, user: user.slice(:id, :name, :inheritance_code, :role) }
|
||||
else
|
||||
{ valid: false }
|
||||
end
|
||||
return render json: { valid: false } unless user
|
||||
|
||||
UserIp.find_or_create_by!(user:, ip_address:)
|
||||
|
||||
render json: { valid: true, user: user.slice(:id, :name, :inheritance_code, :role) }
|
||||
end
|
||||
|
||||
def renew
|
||||
|
||||
@@ -13,6 +13,22 @@ class WikiPagesController < ApplicationController
|
||||
render_wiki_page_or_404 WikiPage.find_by(title: params[:title])
|
||||
end
|
||||
|
||||
def exists
|
||||
if WikiPage.exists?(params[:id])
|
||||
head :no_content
|
||||
else
|
||||
head :not_found
|
||||
end
|
||||
end
|
||||
|
||||
def exists_by_title
|
||||
if WikiPage.exists?(title: params[:title])
|
||||
head :no_content
|
||||
else
|
||||
head :not_found
|
||||
end
|
||||
end
|
||||
|
||||
def diff
|
||||
id = params[:id]
|
||||
from = params[:from]
|
||||
|
||||
@@ -6,6 +6,7 @@ class NicoTagRelation < ApplicationRecord
|
||||
validates :tag_id, presence: true
|
||||
|
||||
validate :nico_tag_must_be_nico
|
||||
validate :tag_mustnt_be_nico
|
||||
|
||||
private
|
||||
|
||||
|
||||
@@ -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',
|
||||
@@ -15,6 +17,8 @@ class Post < ApplicationRecord
|
||||
foreign_key: :target_post_id
|
||||
has_one_attached :thumbnail
|
||||
|
||||
validate :validate_original_created_range
|
||||
|
||||
def as_json options = { }
|
||||
super(options).merge({ thumbnail: thumbnail.attached? ?
|
||||
Rails.application.routes.url_helpers.rails_blob_url(
|
||||
@@ -49,4 +53,20 @@ class Post < ApplicationRecord
|
||||
filename: 'resized_thumbnail.jpg',
|
||||
content_type: 'image/jpeg')
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def validate_original_created_range
|
||||
f = original_created_from
|
||||
b = original_created_before
|
||||
return if f.blank? || b.blank?
|
||||
|
||||
f = Time.zone.parse(f) if String === f
|
||||
b = Time.zone.parse(b) if String === b
|
||||
return if !(f) || !(b)
|
||||
|
||||
if f >= b
|
||||
errors.add :original_created_before, 'オリジナルの作成日時の順番がをかしぃです.'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -11,6 +13,14 @@ class Tag < ApplicationRecord
|
||||
dependent: :destroy
|
||||
has_many :linked_nico_tags, through: :reversed_nico_tag_relations, source: :nico_tag
|
||||
|
||||
has_many :tag_implications, foreign_key: :parent_tag_id, dependent: :destroy
|
||||
has_many :children, through: :tag_implications, source: :tag
|
||||
|
||||
has_many :reversed_tag_implications, class_name: 'TagImplication',
|
||||
foreign_key: :tag_id,
|
||||
dependent: :destroy
|
||||
has_many :parents, through: :reversed_tag_implications, source: :parent_tag
|
||||
|
||||
enum :category, { deerjikist: 'deerjikist',
|
||||
meme: 'meme',
|
||||
character: 'character',
|
||||
@@ -35,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
|
||||
@@ -57,10 +67,33 @@ class Tag < ApplicationRecord
|
||||
end
|
||||
end
|
||||
end
|
||||
tags << Tag.tagme if with_tagme && tags.size < 20 && tags.none?(Tag.tagme)
|
||||
tags << Tag.tagme if with_tagme && tags.size < 10 && tags.none?(Tag.tagme)
|
||||
tags.uniq
|
||||
end
|
||||
|
||||
def self.expand_parent_tags tags
|
||||
return [] if tags.blank?
|
||||
|
||||
seen = Set.new
|
||||
result = []
|
||||
stack = tags.compact.dup
|
||||
|
||||
until stack.empty?
|
||||
tag = stack.pop
|
||||
next unless tag
|
||||
|
||||
tag.parents.each do |parent|
|
||||
next if seen.include?(parent.id)
|
||||
|
||||
seen << parent.id
|
||||
result << parent
|
||||
stack << parent
|
||||
end
|
||||
end
|
||||
|
||||
(result + tags).uniq { |t| t.id }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def nico_tag_name_must_start_with_nico
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
class TagImplication < ApplicationRecord
|
||||
belongs_to :tag, class_name: 'Tag'
|
||||
belongs_to :parent_tag, class_name: 'Tag'
|
||||
|
||||
validates :tag_id, presence: true, uniqueness: { scope: :parent_tag_id }
|
||||
validates :parent_tag_id, presence: true
|
||||
|
||||
validate :parent_tag_mustnt_be_itself
|
||||
|
||||
private
|
||||
|
||||
def parent_tag_mustnt_be_itself
|
||||
if parent_tag == tag
|
||||
errors.add :parent_tag_id, '親タグは子タグと同一であってはなりません.'
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -8,7 +8,6 @@ class User < ApplicationRecord
|
||||
|
||||
has_many :posts
|
||||
has_many :settings
|
||||
has_many :ip_addresses
|
||||
has_many :user_ips, dependent: :destroy
|
||||
has_many :ip_addresses, through: :user_ips
|
||||
has_many :user_post_views, dependent: :destroy
|
||||
|
||||
+50
-35
@@ -1,45 +1,60 @@
|
||||
Rails.application.routes.draw do
|
||||
get 'tags/nico', to: 'nico_tags#index'
|
||||
put 'tags/nico/:id', to: 'nico_tags#update'
|
||||
get 'tags/autocomplete', to: 'tags#autocomplete'
|
||||
get 'tags/name/:name', to: 'tags#show_by_name'
|
||||
get 'posts/random', to: 'posts#random'
|
||||
post 'posts/:id/viewed', to: 'posts#viewed'
|
||||
delete 'posts/:id/viewed', to: 'posts#unviewed'
|
||||
get 'preview/title', to: 'preview#title'
|
||||
get 'preview/thumbnail', to: 'preview#thumbnail'
|
||||
get 'wiki/title/:title', to: 'wiki_pages#show_by_title'
|
||||
get 'wiki/search', to: 'wiki_pages#search'
|
||||
get 'wiki/changes', to: 'wiki_pages#changes'
|
||||
get 'wiki/:id/diff', to: 'wiki_pages#diff'
|
||||
get 'wiki/:id', to: 'wiki_pages#show'
|
||||
get 'wiki', to: 'wiki_pages#index'
|
||||
post 'wiki', to: 'wiki_pages#create'
|
||||
put 'wiki/:id', to: 'wiki_pages#update'
|
||||
post 'users/code/renew', to: 'users#renew'
|
||||
resources :nico_tags, path: 'tags/nico', only: [:index, :update]
|
||||
|
||||
resources :tags do
|
||||
collection do
|
||||
get :autocomplete
|
||||
get 'name/:name', action: :show_by_name
|
||||
end
|
||||
end
|
||||
|
||||
scope :preview, controller: :preview do
|
||||
get :title
|
||||
get :thumbnail
|
||||
end
|
||||
|
||||
resources :wiki_pages, path: 'wiki', only: [:index, :show, :create, :update] do
|
||||
collection do
|
||||
get :search
|
||||
get :changes
|
||||
|
||||
scope :title do
|
||||
get ':title/exists', action: :exists_by_title
|
||||
get ':title', action: :show_by_title
|
||||
end
|
||||
end
|
||||
|
||||
member do
|
||||
get :exists
|
||||
get :diff
|
||||
end
|
||||
end
|
||||
|
||||
resources :posts do
|
||||
collection do
|
||||
get :random
|
||||
get :changes
|
||||
end
|
||||
|
||||
member do
|
||||
post :viewed
|
||||
delete :viewed, action: :unviewed
|
||||
end
|
||||
end
|
||||
|
||||
resources :users, only: [:create, :update] do
|
||||
collection do
|
||||
post :verify
|
||||
get :me
|
||||
post 'code/renew', action: :renew
|
||||
end
|
||||
end
|
||||
|
||||
resources :posts
|
||||
resources :ip_addresses
|
||||
resources :nico_tag_relations
|
||||
resources :post_tags
|
||||
resources :settings
|
||||
resources :tag_aliases
|
||||
resources :tags
|
||||
resources :user_ips
|
||||
resources :user_post_views
|
||||
resources :users, only: [:create, :update] do
|
||||
collection do
|
||||
post :verify
|
||||
get :me
|
||||
end
|
||||
end
|
||||
|
||||
# Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html
|
||||
|
||||
# Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500.
|
||||
# Can be used by load balancers and uptime monitors to verify that the app is live.
|
||||
# get "up" => "rails/health#show", as: :rails_health_check
|
||||
|
||||
# Defines the root path route ("/")
|
||||
# root "posts#index"
|
||||
end
|
||||
|
||||
@@ -6,3 +6,7 @@ set :output, standard: '/var/log/btrc_hub_nico_sync.log',
|
||||
every 1.day, at: '3:00 pm' do
|
||||
rake 'nico:sync', environment: 'production'
|
||||
end
|
||||
|
||||
every 1.day, at: '0:00 am' do
|
||||
rake 'post_similarity:calc', environment: 'production'
|
||||
end
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
class AddOriginalCreatedAtToPosts < ActiveRecord::Migration[8.0]
|
||||
def change
|
||||
add_column :posts, :original_created_from, :datetime, after: :created_at
|
||||
add_column :posts, :original_created_before, :datetime, after: :original_created_from
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,9 @@
|
||||
class CreateTagImplications < ActiveRecord::Migration[8.0]
|
||||
def change
|
||||
create_table :tag_implications do |t|
|
||||
t.references :tag, null: false, foreign_key: { to_table: :tags }
|
||||
t.references :parent_tag, null: false, foreign_key: { to_table: :tags }
|
||||
t.timestamps
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -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
|
||||
@@ -0,0 +1,5 @@
|
||||
class RenameIpAdressColumnToIpAddresses < ActiveRecord::Migration[8.0]
|
||||
def change
|
||||
rename_column :ip_addresses, :ip_adress, :ip_address
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,27 @@
|
||||
class AddUniqueIndexToTagImplications < ActiveRecord::Migration[8.0]
|
||||
def up
|
||||
execute <<~SQL
|
||||
DELETE
|
||||
ti1
|
||||
FROM
|
||||
tag_implications ti1
|
||||
INNER JOIN
|
||||
tag_implications ti2
|
||||
ON
|
||||
ti1.tag_id = ti2.tag_id
|
||||
AND ti1.parent_tag_id = ti2.parent_tag_id
|
||||
AND ti1.id > ti2.id
|
||||
;
|
||||
SQL
|
||||
|
||||
add_index :tag_implications, [:tag_id, :parent_tag_id],
|
||||
unique: true,
|
||||
name: 'index_tag_implications_on_tag_id_and_parent_tag_id'
|
||||
end
|
||||
|
||||
def down
|
||||
# NOTE: 重複削除は復元されなぃ.
|
||||
remove_index :tag_implications,
|
||||
name: 'index_tag_implications_on_tag_id_and_parent_tag_id'
|
||||
end
|
||||
end
|
||||
Generated
+23
-2
@@ -10,7 +10,7 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[8.0].define(version: 2025_07_29_210600) do
|
||||
ActiveRecord::Schema[8.0].define(version: 2025_12_10_123200) do
|
||||
create_table "active_storage_attachments", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.string "name", null: false
|
||||
t.string "record_type", null: false
|
||||
@@ -40,7 +40,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_07_29_210600) do
|
||||
end
|
||||
|
||||
create_table "ip_addresses", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.binary "ip_adress", limit: 16, null: false
|
||||
t.binary "ip_address", limit: 16, null: false
|
||||
t.boolean "banned", default: false, null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
@@ -70,9 +70,16 @@ ActiveRecord::Schema[8.0].define(version: 2025_07_29_210600) do
|
||||
t.bigint "deleted_user_id"
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.datetime "discarded_at"
|
||||
t.virtual "is_active", type: :boolean, as: "(`discarded_at` is null)", stored: true
|
||||
t.virtual "active_unique_key", type: :string, as: "(case when (`discarded_at` is null) then concat(`post_id`,_utf8mb4':',`tag_id`) else NULL end)", stored: true
|
||||
t.index ["active_unique_key"], name: "idx_post_tags_active_unique", unique: true
|
||||
t.index ["created_user_id"], name: "index_post_tags_on_created_user_id"
|
||||
t.index ["deleted_user_id"], name: "index_post_tags_on_deleted_user_id"
|
||||
t.index ["discarded_at"], name: "index_post_tags_on_discarded_at"
|
||||
t.index ["post_id", "discarded_at"], name: "index_post_tags_on_post_id_and_discarded_at"
|
||||
t.index ["post_id"], name: "index_post_tags_on_post_id"
|
||||
t.index ["tag_id", "discarded_at"], name: "index_post_tags_on_tag_id_and_discarded_at"
|
||||
t.index ["tag_id"], name: "index_post_tags_on_tag_id"
|
||||
end
|
||||
|
||||
@@ -83,6 +90,8 @@ ActiveRecord::Schema[8.0].define(version: 2025_07_29_210600) do
|
||||
t.bigint "parent_id"
|
||||
t.bigint "uploaded_user_id"
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "original_created_from"
|
||||
t.datetime "original_created_before"
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["parent_id"], name: "index_posts_on_parent_id"
|
||||
t.index ["uploaded_user_id"], name: "index_posts_on_uploaded_user_id"
|
||||
@@ -105,6 +114,16 @@ ActiveRecord::Schema[8.0].define(version: 2025_07_29_210600) do
|
||||
t.index ["tag_id"], name: "index_tag_aliases_on_tag_id"
|
||||
end
|
||||
|
||||
create_table "tag_implications", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.bigint "tag_id", null: false
|
||||
t.bigint "parent_tag_id", null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["parent_tag_id"], name: "index_tag_implications_on_parent_tag_id"
|
||||
t.index ["tag_id", "parent_tag_id"], name: "index_tag_implications_on_tag_id_and_parent_tag_id", unique: true
|
||||
t.index ["tag_id"], name: "index_tag_implications_on_tag_id"
|
||||
end
|
||||
|
||||
create_table "tag_similarities", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||
t.bigint "tag_id", null: false
|
||||
t.bigint "target_tag_id", null: false
|
||||
@@ -174,6 +193,8 @@ ActiveRecord::Schema[8.0].define(version: 2025_07_29_210600) do
|
||||
add_foreign_key "posts", "users", column: "uploaded_user_id"
|
||||
add_foreign_key "settings", "users"
|
||||
add_foreign_key "tag_aliases", "tags"
|
||||
add_foreign_key "tag_implications", "tags"
|
||||
add_foreign_key "tag_implications", "tags", column: "parent_tag_id"
|
||||
add_foreign_key "tag_similarities", "tags"
|
||||
add_foreign_key "tag_similarities", "tags", column: "target_tag_id"
|
||||
add_foreign_key "user_ips", "ip_addresses"
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
namespace :nico do
|
||||
desc 'ニコタグ連携'
|
||||
task link: :environment do
|
||||
Post.find_each do |post|
|
||||
tags = post.tags.where(category: 'nico')
|
||||
tags.each do |tag|
|
||||
post.tags.concat(tag.linked_tags) if tag.linked_tags.present?
|
||||
end
|
||||
post.tags = post.tags.to_a.uniq
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -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 < 20
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user