Merge branch 'main' into #92

This commit is contained in:
2026-01-07 03:24:44 +09:00
19 changed files with 588 additions and 177 deletions
+1 -1
View File
@@ -88,7 +88,7 @@ class PostsController < ApplicationController
original_created_from = params[:original_created_from] original_created_from = params[:original_created_from]
original_created_before = params[:original_created_before] 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: nil, uploaded_user: current_user,
original_created_from:, original_created_before:) original_created_from:, original_created_before:)
post.thumbnail.attach(thumbnail) post.thumbnail.attach(thumbnail)
if post.save if post.save
+101 -62
View File
@@ -1,8 +1,8 @@
class WikiPagesController < ApplicationController class WikiPagesController < ApplicationController
def index rescue_from Wiki::Commit::Conflict, with: :render_wiki_conflict
wiki_pages = WikiPage.all
render json: wiki_pages def index
render json: WikiPage.all
end end
def show def show
@@ -31,21 +31,25 @@ class WikiPagesController < ApplicationController
def diff def diff
id = params[:id] id = params[:id]
from = params[:from] return head :bad_request if id.blank?
from = params[:from].presence
to = params[:to].presence to = params[:to].presence
return head :bad_request if id.blank? || from.blank?
wiki_page_from = WikiPage.find(id) page = WikiPage.find(id)
wiki_page_to = WikiPage.find(id)
wiki_page_from.sha = from
wiki_page_to.sha = to
diffs = Diff::LCS.sdiff(wiki_page_from.body, wiki_page_to.body) from_rev = from && page.wiki_revisions.find(from)
to_rev = to ? page.wiki_revisions.find(to) : page.current_revision
if ((from_rev && !(from_rev.content?)) || !(to_rev&.content?))
return head :unprocessable_entity
end
diffs = Diff::LCS.sdiff(from_rev&.body&.lines || [], to_rev.body.lines)
diff_json = diffs.map { |change| diff_json = diffs.map { |change|
case change.action case change.action
when ?= when ?=
{ type: 'context', content: change.old_element } { type: 'context', content: change.old_element }
when ?| when ?!
[{ type: 'removed', content: change.old_element }, [{ type: 'removed', content: change.old_element },
{ type: 'added', content: change.new_element }] { type: 'added', content: change.new_element }]
when ?+ when ?+
@@ -55,23 +59,32 @@ class WikiPagesController < ApplicationController
end end
}.flatten.compact }.flatten.compact
render json: { wiki_page_id: wiki_page_from.id, render json: { wiki_page_id: page.id,
title: wiki_page_from.title, title: page.title,
older_sha: wiki_page_from.sha, older_revision_id: from_rev&.id,
newer_sha: wiki_page_to.sha, newer_revision_id: to_rev.id,
diff: diff_json } diff: diff_json }
end end
def create def create
return head :unauthorized unless current_user return head :unauthorized unless current_user
return head :forbidden unless ['admin', 'member'].include?(current_user.role) return head :forbidden unless current_user.member?
wiki_page = WikiPage.new(title: params[:title], created_user: current_user, updated_user: current_user) title = params[:title]&.strip
if wiki_page.save body = params[:body].to_s
wiki_page.set_body params[:body], user: current_user
render json: wiki_page, status: :created return head :unprocessable_entity if title.blank? || body.blank?
page = WikiPage.new(title:, created_user: current_user, updated_user: current_user)
if page.save
message = params[:message].presence
Wiki::Commit.content!(page:, body:, created_user: current_user, message:)
render json: page, status: :created
else else
render json: { errors: wiki_page.errors.full_messages }, status: :unprocessable_entity render json: { errors: page.errors.full_messages },
status: :unprocessable_entity
end end
end end
@@ -79,16 +92,24 @@ class WikiPagesController < ApplicationController
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?
title = params[:title] title = params[:title]&.strip
body = params[:body] body = params[:body].to_s
return head :unprocessable_entity if title.blank? || body.blank? return head :unprocessable_entity if title.blank? || body.blank?
wiki_page = WikiPage.find(params[:id]) page = WikiPage.find(params[:id])
wiki_page.title = title base_revision_id = page.current_revision.id
wiki_page.updated_user = current_user
wiki_page.set_body(body, user: current_user) if params[:title].present? && params[:title].strip != page.title
wiki_page.save! return head :unprocessable_entity
end
message = params[:message].presence
Wiki::Commit.content!(page:,
body:,
created_user: current_user,
message:,
base_revision_id:)
head :ok head :ok
end end
@@ -97,55 +118,73 @@ class WikiPagesController < ApplicationController
title = params[:title]&.strip title = params[:title]&.strip
q = WikiPage.all q = WikiPage.all
q = q.where('title LIKE ?', "%#{ WikiPage.sanitize_sql_like(title) }%") if title.present? if title.present?
q = q.where('title LIKE ?', "%#{ WikiPage.sanitize_sql_like(title) }%")
end
render json: q.limit(20) render json: q.limit(20)
end end
def changes def changes
id = params[:id] id = params[:id].presence
log = if id.present? q = WikiRevision.includes(:wiki_page, :created_user).order(id: :desc)
wiki.page("#{ id }.md")&.versions q = q.where(wiki_page_id: id) if id
else
wiki.repo.log('main', nil)
end
return render json: [] unless log
render json: log.map { |commit| render json: q.limit(200).map { |rev|
wiki_page = WikiPage.find(commit.message.split(' ')[1].to_i) { revision_id: rev.id,
wiki_page.sha = commit.id pred: rev.base_revision_id,
succ: nil,
next nil if wiki_page.sha.blank? wiki_page: { id: rev.wiki_page_id, title: rev.wiki_page.title },
user: { id: rev.created_user.id, name: rev.created_user.name },
user = User.find(commit.author.name.to_i) kind: rev.kind,
message: rev.message,
{ sha: wiki_page.sha, timestamp: rev.created_at }
pred: wiki_page.pred,
succ: wiki_page.succ,
wiki_page: wiki_page && { id: wiki_page.id, title: wiki_page.title },
user: user && { id: user.id, name: user.name },
change_type: commit.message.split(' ')[0].downcase[0...(-1)],
timestamp: commit.authored_date }
}.compact }.compact
end end
private private
WIKI_PATH = Rails.root.join('wiki').to_s def render_wiki_page_or_404 page
return head :not_found unless page
def wiki if params[:version].present?
@wiki ||= Gollum::Wiki.new(WIKI_PATH) rev = page.wiki_revisions.find_by(id: params[:version])
return head :not_found unless rev
if rev.redirect?
return (
redirect_to wiki_page_by_title_path(title: rev.redirect_page.title),
status: :moved_permanently)
end end
def render_wiki_page_or_404 wiki_page body = rev.body
return head :not_found unless wiki_page revision_id = rev.id
pred = page.pred_revision_id(revision_id)
succ = page.succ_revision_id(revision_id)
wiki_page.sha = params[:version].presence return render json: page.as_json.merge(body:, revision_id:, pred:, succ:)
end
body = wiki_page.body rev = page.current_revision
sha = wiki_page.sha unless rev
pred = wiki_page.pred return render json: page.as_json.merge(body: nil, revision_id: nil, pred: nil, succ: nil)
succ = wiki_page.succ end
render json: wiki_page.as_json.merge(body:, sha:, pred:, succ:)
if rev.redirect?
return (
redirect_to wiki_page_by_title_path(title: rev.redirect_page.title),
status: :moved_permanently)
end
body = rev.body
revision_id = rev.id
pred = page.pred_revision_id(revision_id)
succ = page.succ_revision_id(revision_id)
render json: page.as_json.merge(body:, revision_id:, pred:, succ:)
end
def render_wiki_conflict err
render json: { error: 'conflict', message: err.message }, status: :conflict
end end
end end
+15
View File
@@ -0,0 +1,15 @@
class WikiLine < ApplicationRecord
has_many :wiki_revision_lines, dependent: :restrict_with_exception
validates :sha256, presence: true, uniqueness: true, length: { is: 64 }
validates :body, presence: true
def self.upsert_by_body! body
sha = Digest::SHA256.hexdigest(body)
now = Time.current
upsert({ sha256: sha, body:, created_at: now, updated_at: now })
find_by!(sha256: sha)
end
end
+33 -63
View File
@@ -1,80 +1,50 @@
require 'gollum-lib' require 'set'
class WikiPage < ApplicationRecord class WikiPage < ApplicationRecord
belongs_to :tag, optional: true has_many :wiki_revisions, dependent: :destroy
belongs_to :created_user, class_name: 'User', foreign_key: 'created_user_id' belongs_to :created_user, class_name: 'User'
belongs_to :updated_user, class_name: 'User', foreign_key: 'updated_user_id' belongs_to :updated_user, class_name: 'User'
has_many :redirected_from_revisions,
class_name: 'WikiRevision',
foreign_key: :redirect_page_id,
dependent: :nullify
validates :title, presence: true, length: { maximum: 255 }, uniqueness: true validates :title, presence: true, length: { maximum: 255 }, uniqueness: true
def as_json options = { } def current_revision
self.sha = nil wiki_revisions.order(id: :desc).first
super options
end
def sha= val
if val.present?
@sha = val
@page = wiki.page("#{ id }.md", @sha)
else
@page = wiki.page("#{ id }.md")
@sha = @page.versions.first.id
end
vers = @page.versions
idx = vers.find_index { |ver| ver.id == @sha }
if idx
@pred = vers[idx + 1]&.id
@succ = idx.positive? ? vers[idx - 1].id : nil
@updated_at = vers[idx].authored_date
else
@sha = nil
@pred = nil
@succ = nil
@updated_at = nil
end
@sha
end
def sha
@sha
end
def pred
@pred
end
def succ
@succ
end
def updated_at
@updated_at
end end
def body def body
sha = nil unless @page rev = current_revision
@page&.raw_data&.force_encoding('UTF-8') rev.body if rev&.content?
end end
def set_body content, user: def resolve_redirect limit: 10
commit_info = { name: user.id.to_s, page = self
email: 'dummy@example.com' } visited = Set.new
page = wiki.page("#{ id }.md")
if page limit.times do
commit_info[:message] = "Updated #{ id }" return page if visited.include?(page.id)
wiki.update_page(page, id.to_s, :markdown, content, commit_info)
else visited.add(page.id)
commit_info[:message] = "Created #{ id }"
wiki.write_page(id.to_s, :markdown, content, commit_info) rev = page.current_revision
end return page if !(rev&.redirect?) || !(rev.redirect_page)
page = rev.redirect_page
end end
private page
end
WIKI_PATH = Rails.root.join('wiki').to_s def pred_revision_id revision_id
wiki_revisions.where('id < ?', revision_id).order(id: :desc).limit(1).pick(:id)
end
def wiki def succ_revision_id revision_id
@wiki ||= Gollum::Wiki.new(WIKI_PATH) wiki_revisions.where('id > ?', revision_id).order(id: :asc).limit(1).pick(:id)
end end
end end
+55
View File
@@ -0,0 +1,55 @@
class WikiRevision < ApplicationRecord
belongs_to :wiki_page
belongs_to :base_revision, class_name: 'WikiRevision', optional: true
belongs_to :created_user, class_name: 'User'
belongs_to :redirect_page, class_name: 'WikiPage', optional: true
has_many :wiki_revision_lines, dependent: :delete_all
has_many :wiki_lines, through: :wiki_revision_lines
enum :kind, { content: 0, redirect: 1 }
validates :kind, presence: true
validates :lines_count, numericality: { only_integer: true, greater_than_or_equal_to: 0 }
validates :tree_sha256, length: { is: 64 }, allow_nil: true
validate :kind_consistency
def body
return unless content?
wiki_revision_lines
.includes(:wiki_line)
.order(:position)
.map { |rev| rev.wiki_line.body }
.join("\n")
end
private
def kind_consistency
if content?
if tree_sha256.blank?
errors.add(:tree_sha256, '種類がページの場合は必須です.')
end
if redirect_page_id.present?
errors.add(:redirect_page_id, '種類がページの場合は空である必要があります.')
end
end
if redirect?
if redirect_page_id.blank?
errors.add(:redirect_page_id, '種類がリダイレクトの場合は必須です.')
end
if tree_sha256.present?
errors.add(:tree_sha256, '種類がリダイレクトの場合は空である必要があります.')
end
if lines_count.to_i > 0
errors.add(:lines_count, '種類がリダイレクトの場合は 0 である必要があります.')
end
end
end
end
+8
View File
@@ -0,0 +1,8 @@
class WikiRevisionLine < ApplicationRecord
belongs_to :wiki_revision
belongs_to :wiki_line
validates :position, presence: true,
numericality: { only_integer: true, greater_than_or_equal_to: 0 }
validates :position, uniqueness: { scope: :wiki_revision_id }
end
+122
View File
@@ -0,0 +1,122 @@
require 'digest'
module Wiki
class Commit
class Conflict < StandardError
;
end
def self.content! page:, body:, created_user:, message: nil, base_revision_id: nil
new(page:, created_user:).content!(body:, message:, base_revision_id:)
end
def self.redirect! page:, redirect_page:, created_user:, message: nil, base_revision_id: nil
new(page:, created_user:).redirect!(redirect_page:, message:, base_revision_id:)
end
def initialize page:, created_user:
@page = page
@created_user = created_user
end
def content! body:, message:, base_revision_id:
normalised = normalise_body(body)
lines = split_lines(normalised)
line_shas = lines.map { |line| Digest::SHA256.hexdigest(line) }
tree_sha = Digest::SHA256.hexdigest(line_shas.join(','))
line_id_by_sha = upsert_lines!(lines, line_shas)
line_ids = line_shas.map { |sha| line_id_by_sha.fetch(sha) }
ActiveRecord::Base.transaction do
@page.lock!
if base_revision_id.present?
current_id = @page.wiki_revisions.maximum(:id)
if current_id && current_id != base_revision_id.to_i
raise Conflict,
"競合が発生してゐます(現在の Id.#{ current_id },ベース Id.#{ base_revision_id })."
end
end
rev = WikiRevision.create!(
wiki_page: @page,
base_revision_id:,
created_user: @created_user,
kind: :content,
redirect_page_id: nil,
message:,
lines_count: lines.length,
tree_sha256: tree_sha)
rows = line_ids.each_with_index.map do |line_id, pos|
{ wiki_revision_id: rev.id, wiki_line_id: line_id, position: pos }
end
WikiRevisionLine.insert_all!(rows)
rev
end
end
def redirect! redirect_page:, message:, base_revision_id:
ActiveRecord::Base.transaction do
@page.lock!
if base_revision_id.present?
current_id = @page.wiki_revisions.maximum(:id)
if current_id && current_id != base_revision_id.to_i
raise Conflict,
"競合が発生してゐます(現在の Id.#{ current_id },ベース Id.#{ base_revision_id })."
end
end
WikiRevision.create!(
wiki_page: @page,
base_revision_id:,
created_user: @created_user,
kind: :redirect,
redirect_page:,
message:,
lines_count: 0,
tree_sha256: nil)
end
end
private
def normalise_body body
s = body.to_s
s.gsub!("\r\n", "\n")
s.encode('UTF-8', invalid: :replace, undef: :replace, replace: '🖕')
end
def split_lines body
body.split("\n")
end
def upsert_lines! lines, line_shas
now = Time.current
id_by_sha = WikiLine.where(sha256: line_shas).pluck(:sha256, :id).to_h
missing_rows = []
line_shas.each_with_index do |sha, i|
next if id_by_sha.key?(sha)
missing_rows << { sha256: sha,
body: lines[i],
created_at: now,
updated_at: now }
end
if missing_rows.any?
WikiLine.upsert_all(missing_rows)
id_by_sha = WikiLine.where(sha256: line_shas).pluck(:sha256, :id).to_h
end
id_by_sha
end
end
end
@@ -0,0 +1,5 @@
class RemoveTagFromWikiPages < ActiveRecord::Migration[7.0]
def change
remove_reference :wiki_pages, :tag, if_exists: true
end
end
@@ -0,0 +1,11 @@
class CreateWikiLines < ActiveRecord::Migration[7.0]
def change
create_table :wiki_lines do |t|
t.string :sha256, null: false, limit: 64
t.text :body, null: false
t.timestamps
end
add_index :wiki_lines, :sha256, unique: true
end
end
@@ -0,0 +1,19 @@
class CreateWikiRevisions < ActiveRecord::Migration[7.0]
def change
create_table :wiki_revisions do |t|
t.references :wiki_page, null: false, foreign_key: true
t.references :base_revision, foreign_key: { to_table: :wiki_revisions }
t.references :created_user, null: false, foreign_key: { to_table: :users }
t.integer :kind, null: false, default: 0 # 0: content, 1: redirect
t.references :redirect_page, foreign_key: { to_table: :wiki_pages }
t.string :message
t.integer :lines_count, null: false, default: 0
t.string :tree_sha256, limit: 64
t.timestamps
end
add_index :wiki_revisions, :tree_sha256
add_index :wiki_revisions, [:wiki_page_id, :id]
add_index :wiki_revisions, :kind
end
end
@@ -0,0 +1,12 @@
class CreateWikiRevisionLines < ActiveRecord::Migration[7.0]
def change
create_table :wiki_revision_lines do |t|
t.references :wiki_revision, null: false, foreign_key: true
t.integer :position, null: false
t.references :wiki_line, null: false, foreign_key: true
end
add_index :wiki_revision_lines, [:wiki_revision_id, :position], unique: true
add_index :wiki_revision_lines, [:wiki_revision_id, :wiki_line_id]
end
end
@@ -0,0 +1,27 @@
class MakeThumbnailBaseNullableInPosts < ActiveRecord::Migration[7.0]
def up
change_column_null :posts, :thumbnail_base, true
execute <<~SQL
UPDATE
posts
SET
thumbnail_base = NULL
WHERE
thumbnail_base = ''
SQL
end
def down
execute <<~SQL
UPDATE
posts
SET
thumbnail_base = ''
WHERE
thumbnail_base IS NULL
SQL
change_column_null :posts, :thumbnail_base, false
end
end
+46 -5
View File
@@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[8.0].define(version: 2025_12_10_123200) do ActiveRecord::Schema[8.0].define(version: 2025_12_30_143400) do
create_table "active_storage_attachments", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| create_table "active_storage_attachments", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.string "name", null: false t.string "name", null: false
t.string "record_type", null: false t.string "record_type", null: false
@@ -86,7 +86,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_12_10_123200) do
create_table "posts", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| create_table "posts", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.string "title", null: false t.string "title", null: false
t.string "url", limit: 2000, null: false t.string "url", limit: 2000, null: false
t.string "thumbnail_base", limit: 2000, null: false t.string "thumbnail_base", limit: 2000
t.bigint "parent_id" t.bigint "parent_id"
t.bigint "uploaded_user_id" t.bigint "uploaded_user_id"
t.datetime "created_at", null: false t.datetime "created_at", null: false
@@ -167,18 +167,54 @@ ActiveRecord::Schema[8.0].define(version: 2025_12_10_123200) do
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
end end
create_table "wiki_lines", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.string "sha256", limit: 64, null: false
t.text "body", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["sha256"], name: "index_wiki_lines_on_sha256", unique: true
end
create_table "wiki_pages", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| create_table "wiki_pages", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.string "title", null: false t.string "title", null: false
t.bigint "tag_id"
t.bigint "created_user_id", null: false t.bigint "created_user_id", null: false
t.bigint "updated_user_id", null: false t.bigint "updated_user_id", null: false
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.index ["created_user_id"], name: "index_wiki_pages_on_created_user_id" t.index ["created_user_id"], name: "index_wiki_pages_on_created_user_id"
t.index ["tag_id"], name: "index_wiki_pages_on_tag_id"
t.index ["updated_user_id"], name: "index_wiki_pages_on_updated_user_id" t.index ["updated_user_id"], name: "index_wiki_pages_on_updated_user_id"
end end
create_table "wiki_revision_lines", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.bigint "wiki_revision_id", null: false
t.integer "position", null: false
t.bigint "wiki_line_id", null: false
t.index ["wiki_line_id"], name: "index_wiki_revision_lines_on_wiki_line_id"
t.index ["wiki_revision_id", "position"], name: "index_wiki_revision_lines_on_wiki_revision_id_and_position", unique: true
t.index ["wiki_revision_id", "wiki_line_id"], name: "index_wiki_revision_lines_on_wiki_revision_id_and_wiki_line_id"
t.index ["wiki_revision_id"], name: "index_wiki_revision_lines_on_wiki_revision_id"
end
create_table "wiki_revisions", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.bigint "wiki_page_id", null: false
t.bigint "base_revision_id"
t.bigint "created_user_id", null: false
t.integer "kind", default: 0, null: false
t.bigint "redirect_page_id"
t.string "message"
t.integer "lines_count", default: 0, null: false
t.string "tree_sha256", limit: 64
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["base_revision_id"], name: "index_wiki_revisions_on_base_revision_id"
t.index ["created_user_id"], name: "index_wiki_revisions_on_created_user_id"
t.index ["kind"], name: "index_wiki_revisions_on_kind"
t.index ["redirect_page_id"], name: "index_wiki_revisions_on_redirect_page_id"
t.index ["tree_sha256"], name: "index_wiki_revisions_on_tree_sha256"
t.index ["wiki_page_id", "id"], name: "index_wiki_revisions_on_wiki_page_id_and_id"
t.index ["wiki_page_id"], name: "index_wiki_revisions_on_wiki_page_id"
end
add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id" add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"
add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id" add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id"
add_foreign_key "nico_tag_relations", "tags" add_foreign_key "nico_tag_relations", "tags"
@@ -201,7 +237,12 @@ ActiveRecord::Schema[8.0].define(version: 2025_12_10_123200) do
add_foreign_key "user_ips", "users" add_foreign_key "user_ips", "users"
add_foreign_key "user_post_views", "posts" add_foreign_key "user_post_views", "posts"
add_foreign_key "user_post_views", "users" add_foreign_key "user_post_views", "users"
add_foreign_key "wiki_pages", "tags"
add_foreign_key "wiki_pages", "users", column: "created_user_id" add_foreign_key "wiki_pages", "users", column: "created_user_id"
add_foreign_key "wiki_pages", "users", column: "updated_user_id" add_foreign_key "wiki_pages", "users", column: "updated_user_id"
add_foreign_key "wiki_revision_lines", "wiki_lines"
add_foreign_key "wiki_revision_lines", "wiki_revisions"
add_foreign_key "wiki_revisions", "users", column: "created_user_id"
add_foreign_key "wiki_revisions", "wiki_pages"
add_foreign_key "wiki_revisions", "wiki_pages", column: "redirect_page_id"
add_foreign_key "wiki_revisions", "wiki_revisions", column: "base_revision_id"
end end
+74
View File
@@ -0,0 +1,74 @@
namespace :wiki do
desc 'Wiki 移行'
task migrate: :environment do
require 'digest'
require 'gollum-lib'
wiki = Gollum::Wiki.new(Rails.root.join('wiki').to_s)
WikiPage.where.missing(:wiki_revisions).find_each do |wiki_page|
page = wiki.page("#{ wiki_page.id }.md")
next unless page
versions = page.versions
next if versions.blank?
base_revision_id = nil
versions.reverse_each do |version|
pg = wiki.page("#{ wiki_page.id }.md", version.id)
raw = pg&.raw_data
next unless raw
lines = raw.force_encoding('UTF-8').split("\n")
line_shas = lines.map { |l| Digest::SHA256.hexdigest(l) }
tree_sha = Digest::SHA256.hexdigest(line_shas.join(','))
at = version.authored_date
line_id_by_sha = WikiLine.where(sha256: line_shas).pluck(:sha256, :id).to_h
missing_rows = []
line_shas.each_with_index do |sha, i|
next if line_id_by_sha.key?(sha)
missing_rows << { sha256: sha,
body: lines[i],
created_at: at,
updated_at: at }
end
if missing_rows.any?
WikiLine.upsert_all(missing_rows)
line_id_by_sha = WikiLine.where(sha256: line_shas).pluck(:sha256, :id).to_h
end
line_ids = line_shas.map { |sha| line_id_by_sha.fetch(sha) }
rev = nil
ActiveRecord::Base.transaction do
wiki_page.lock!
rev = WikiRevision.create!(
wiki_page:,
base_revision_id:,
created_user_id: (Integer(version.author.name) rescue 2),
kind: :content,
redirect_page_id: nil,
message: nil,
lines_count: lines.length,
tree_sha256: tree_sha,
created_at: at,
updated_at: at)
rows = line_ids.each_with_index.map do |line_id, pos|
{ wiki_revision_id: rev.id,
wiki_line_id: line_id,
position: pos }
end
WikiRevisionLine.insert_all!(rows)
end
base_revision_id = rev.id
end
end
end
end
+1 -1
View File
@@ -49,7 +49,7 @@ namespace :nico do
unless post unless post
title = datum['title'] title = datum['title']
url = "https://www.nicovideo.jp/watch/#{ datum['code'] }" url = "https://www.nicovideo.jp/watch/#{ datum['code'] }"
thumbnail_base = fetch_thumbnail.(url) || '' rescue '' 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?
post.thumbnail.attach( post.thumbnail.attach(
+24 -5
View File
@@ -1,3 +1,4 @@
import { useState } from 'react'
import YoutubeEmbed from 'react-youtube' import YoutubeEmbed from 'react-youtube'
import NicoViewer from '@/components/NicoViewer' import NicoViewer from '@/components/NicoViewer'
@@ -39,10 +40,28 @@ export default (({ post }: Props) => {
} }
} }
const [framed, setFramed] = useState (false)
return ( return (
<a href={post.url} target="_blank"> <>
<img src={post.thumbnailBase || post.thumbnail} {framed
alt={post.url} ? (
className="mb-4 w-full"/> <iframe
</a>) src={post.url}
title={post.title || post.url}
width={640}
height={360}/>)
: (
<div>
<a href="#" onClick={e => {
e.preventDefault ()
setFramed (confirm ('未確認の外部ページを表示します。\n'
+ '悪意のあるスクリプトが実行される可能性があります。\n'
+ '表示しますか?'))
return
}}>
</a>
</div>)}
</>)
}) satisfies FC<Props> }) satisfies FC<Props>
+3 -3
View File
@@ -40,10 +40,10 @@ export default () => {
{diff {diff
? ( ? (
diff.diff.map (d => ( diff.diff.map (d => (
<span className={cn (d.type === 'added' && 'bg-green-200 dark:bg-green-800', <p className={cn (d.type === 'added' && 'bg-green-200 dark:bg-green-800',
d.type === 'removed' && 'bg-red-200 dark:bg-red-800')}> d.type === 'removed' && 'bg-red-200 dark:bg-red-800')}>
{d.content == '\n' ? <br/> : d.content} {d.content}
</span>))) </p>)))
: 'Loading...'} : 'Loading...'}
</div> </div>
</MainArea>) </MainArea>)
+5 -15
View File
@@ -41,30 +41,20 @@ export default () => {
</thead> </thead>
<tbody> <tbody>
{changes.map (change => ( {changes.map (change => (
<tr key={change.sha}> <tr key={change.revisionId}>
<td> <td>
{change.changeType === 'update' && ( {change.pred != null && (
<Link to={`/wiki/${ change.wikiPage.id }/diff?from=${ change.pred }&to=${ change.sha }`}> <Link to={`/wiki/${ change.wikiPage.id }/diff?from=${ change.pred }&to=${ change.revisionId }`}>
</Link>)} </Link>)}
</td> </td>
<td className="p-2"> <td className="p-2">
<Link to={`/wiki/${ encodeURIComponent (change.wikiPage.title) }?version=${ change.sha }`}> <Link to={`/wiki/${ encodeURIComponent (change.wikiPage.title) }?version=${ change.revisionId }`}>
{change.wikiPage.title} {change.wikiPage.title}
</Link> </Link>
</td> </td>
<td className="p-2"> <td className="p-2">
{(() => { {change.pred == null ? '新規' : '更新'}
switch (change.changeType)
{
case 'create':
return '新規'
case 'update':
return '更新'
case 'delete':
return '削除'
}
}) ()}
</td> </td>
<td className="p-2"> <td className="p-2">
<Link to={`/users/${ change.user.id }`}> <Link to={`/users/${ change.user.id }`}>
+16 -12
View File
@@ -61,26 +61,30 @@ export type ViewFlagBehavior = typeof ViewFlagBehavior[keyof typeof ViewFlagBeha
export type WikiPage = { export type WikiPage = {
id: number id: number
title: string title: string
createdUserId: number
updatedUserId: number
createdAt: string
updatedAt: string
body: string body: string
sha: string revisionId: number
pred?: string pred: number | null
succ?: string succ: number | null }
updatedAt?: string }
export type WikiPageChange = { export type WikiPageChange = {
sha: string revisionId: number
pred?: string pred: number | null
succ?: string succ: null
wikiPage: WikiPage wikiPage: Pick<WikiPage, 'id' | 'title'>
user: User user: Pick<User, 'id' | 'name'>
changeType: string kind: 'content' | 'redirect'
message: string | null
timestamp: string } timestamp: string }
export type WikiPageDiff = { export type WikiPageDiff = {
wikiPageId: number wikiPageId: number
title: string title: string
olderSha: string olderRevisionId: number | null
newerSha: string newerRevisionId: number
diff: WikiPageDiffDiff[] } diff: WikiPageDiffDiff[] }
export type WikiPageDiffDiff = { export type WikiPageDiffDiff = {