From 217bc395e1679dd0d2288500f995bd21e2589171 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=BF=E3=81=A6=E3=82=8B=E3=81=9E?= Date: Sun, 10 May 2026 13:28:13 +0900 Subject: [PATCH] --- ...%9F%E8%A3%85%E8%AA%AC%E6%98%8E%E6%9B%B8.md | 2454 ++++++++--------- 1 file changed, 1151 insertions(+), 1303 deletions(-) diff --git a/%E5%AE%9F%E8%A3%85%E8%AA%AC%E6%98%8E%E6%9B%B8.md b/%E5%AE%9F%E8%A3%85%E8%AA%AC%E6%98%8E%E6%9B%B8.md index 19c435a..c62cb9f 100644 --- a/%E5%AE%9F%E8%A3%85%E8%AA%AC%E6%98%8E%E6%9B%B8.md +++ b/%E5%AE%9F%E8%A3%85%E8%AA%AC%E6%98%8E%E6%9B%B8.md @@ -1,1674 +1,1522 @@ --- -title: 'BTRC Hub 仕様書' -subtitle: '現行仕様と計画仕様の統合版(2026-04-20)' -date: '2026-04-20' +title: 'BTRC Hub / タグ広場 現行実装仕様書' +subtitle: '現行ソース btrc-hub-main.zip に基づく実装事実ベース仕様(2026-05-10)' +date: '2026-05-10' lang: ja-JP toc: true -toc-depth: 2 +toc-depth: 3 numbersections: true -documentclass: report -geometry: - - margin=22mm -fontsize: 10pt -mainfont: 'Noto Sans CJK JP' -monofont: 'Noto Sans Mono CJK JP' -header-includes: - - | - \usepackage{longtable} - \usepackage{booktabs} - \usepackage{array} - \usepackage{xcolor} - \usepackage{hyperref} - \definecolor{linkcolor}{HTML}{0B5FFF} - \hypersetup{colorlinks=true,linkcolor=linkcolor,urlcolor=linkcolor} --- # 本書の位置づけ -本書は、2026-04-19 時点で置かれている資材と、2026-04-20 の追加開発者ヒアリングをもとに、BTRC Hub の仕様を最新化したものである。 +本書は、2026-05-10 時点で添付された `btrc-hub-main.zip` の実装を確認し、タグ広場(BTRC Hub)の現行仕様を再構成した文書である。 -- `btrc-hub-main.zip`: 現行ソースコード -- `btrc-hub.wiki.zip`: 開発 Wiki -- `btrc-hub.issue.sql`: Gitea issue エクスポート -- `BTRC_Hub_仕様書_2026-03-23.md`: 既存仕様書 +既存の `BTRC_Hub_仕様書_2026-03-23.md` は有用な土台だが、現行実装とはすでに差分がある。したがって本書では、過去仕様書の記述よりも現行ソース・`db/schema.rb`・`routes.rb`・フロントエンドルーティング・主要コントローラ/モデルの実装を優先する。 -本書の目的は、実装済みの事実と将来計画を分離して、開発・公開判断・今後の設計変更に耐える基準文書にすることである。 - -## 情報の優先順位 - -| 優先順位 | 情報源 | 扱い | -| ---: | --- | --- | -| 1 | 現行ソースコード、`db/schema.rb`、ルーティング | 現行仕様の根拠 | -| 2 | フロントエンドの画面・型・API 呼出 | 画面仕様と実利用導線の根拠 | -| 3 | Wiki と issue | 設計意図、未実装計画、運用課題の根拠 | -| 4 | 既存仕様書 | 体系化済みの文脈として再利用 | - -本書では、仕様を次の 3 種に分ける。 +本書の分類は次のとおり。 | 表記 | 意味 | | --- | --- | -| 現行仕様 | 現在のコード、スキーマ、画面から確認できるもの | -| 計画仕様(確定) | 実装は未完だが方針として採用済みのもの | -| 計画仕様(未確定) | 方向性はあるが、実装方針・責務・公開範囲が未整理のもの | +| 現行仕様 | 現在のソース・スキーマ・画面で確認できるもの | +| 実装あり・UI薄い | バックエンドやモデルは存在するが、画面や導線が不足しているもの | +| 計画・未確定 | ソースだけでは確定できず、開発者ヒアリングが必要なもの | +| 注意 | 実装上の落とし穴、仕様として明文化すべき制約 | -## 2026-03-23 版からの主要更新 +本書は「願望」ではなく、コードにあるものをまず書く。曖昧な点は `開発者ヒアリング` として末尾に隔離する。 -今回の最新化で、次の項目を現行仕様へ昇格した。 +## 用語: 公開と公表 -| 項目 | 2026-04-19 時点の扱い | +本書では、以後 **公開** と **公表** を分ける。 + +| 用語 | 意味 | | --- | --- | -| 投稿版管理 `post_versions` | 実装済み。`/posts/versions` と耕作履歴画面で使用 | -| 投稿履歴からの復元 | フロントで実装済み。ただし復元は専用 API ではなく `PUT /posts/:id` を使う | -| 素材集 `materials` | API と画面が実装済み。ただしトップナビでは非表示 | -| 素材の版テーブル `material_versions` | テーブルのみ存在。モデル・記録処理・API は未接続 | -| Wiki 画像 `wiki_assets` | テーブルのみ存在。現行ルート・モデル・画面は未接続 | -| 上位タグ D&D 操作 | 投稿詳細サイドバーで実装済み。API は admin のみ許可 | -| タグ階層取得 `/tags/with-depth` | 実装済み。素材サイドバーで使用 | -| 利用規約 `/tos` | MDX ページとルートは存在。ただしトップナビでは非表示 | -| R2 ストレージ | production で Active Storage service `:r2` を使用 | -| whenever スケジュール | `nico:sync` と `post_similarity:calc` が `config/schedule.rb` に定義済み | -| `tag_similarities` | テーブル・モデル・計算タスクは存在。ただし現行 UI での明確な表示導線は薄い | -| 素材作成権限 | 公開時も guest に素材作成を許可する方針 | -| Wiki 画像記法 | 専用記法を採用する方針。具体案は `![alt](asset:1)` 系 | +| 公開 | URL として到達可能であり、検索エンジン等にも拾われ得る状態。現時点のタグ広場はすでにこの状態である。 | +| 公表 | ぼざクリ界隈等へ明示的に告知し、利用導線を出して、人を呼び込む状態。 | -逆に、次のものは現行仕様として書くと嘘になる。 +したがって、旧来の「一般公開前」「公開前」という表現は、本書では原則として **公表前** と読み替える。すでにサービスは外部から到達可能であり、問題は「存在をどこまで告知し、利用を促すか」である。 + +![公開と公表の用語整理](btrc_spec_figures/public_terminology.png) -- Wiki 画像アップロードの実用 UI/API -- 素材履歴の表示・復元 -- 汎用設定 `settings` の取得・保存・反映 API -- BAN の認可強制 -- 投稿編集の排他制御 -- 別名タグ管理 UI -- Wiki 本文検索 -- Theatre の会場一覧・作成・編集 -- 管理画面 -- 通知機能 # システム概要 ## 目的 -BTRC Hub は、ぼざろクリーチャーシリーズ関連コンテンツへのリンクを集約し、タグ・Wiki・素材・外部同期によって横断的に検索・整理するためのシステムである。 +BTRC Hub は、ぼざろクリーチャー関連コンテンツへのリンクを収集し、タグ・Wiki・素材・外部同期・上映会機能を通じて、作品群や関連知識を整理・再発見しやすくするための共同編集型基盤である。 -重要なのは、保持する対象がコンテンツ本体ではなく **リンクと付帯知識** である点である。開発 Wiki でも、danbooru 的な整理性は参考にしつつ、コンテンツ本体の転載ではなくリンク集約に留める方針が示されている。 +中核は次の 5 系統である。 -## 中核価値 - -現行 BTRC Hub の中核価値は次の 5 つである。 - -1. 複数プラットフォーム上の関連コンテンツをリンクとして集約する。 -2. タグ・別名・上位タグ・外部タグ連携で再発見性を高める。 -3. Wiki によりタグ単位の説明と知識を蓄積する。 -4. ニコニコ外部 DB から投稿・タグ情報を日次同期する。 -5. 素材集・上映会など、整理基盤の周辺機能を育てる。 +1. **投稿**: 外部 URL を中心としたリンクデータ。 +2. **タグ**: カテゴリ、別名、親子関係、外部タグ連携、素材連携を持つ分類単位。 +3. **Wiki**: タグ名と結びつく説明ページ。行単位ストアと改訂履歴を持つ。 +4. **素材**: キャラクター・素材タグに紐づくファイルまたは参考 URL。 +5. **上映会**: 投稿を共同視聴し、コメント・在席・ホスト制御を行う実験的機能。 ## 非目的 -現行の実装思想上、次は中核目的ではない。 +現行実装は次を主目的にしていない。 -- 汎用 SNS 化 -- コメント欄中心の交流サービス化 -- コンテンツ本体の保存・転載 -- 動画配信基盤そのものの実装 -- ニジカ Wiki や虹鹿園と同一目的の重複サービス化 +- 汎用 SNS。 +- コメント掲示板。 +- 外部コンテンツ本体の転載保存サービス。 +- 高度な動画配信基盤。 +- 通常のメール/パスワード式アカウント管理。 -ただし、Theatre や素材集のように、整理基盤から派生する共同利用機能は実装され始めている。 +ただし、上映会や素材投稿などにより、単純なリンク集より重い知識基盤へ寄っている。 ## 技術構成 | 層 | 現行構成 | | --- | --- | -| バックエンド | Rails 8.0.2, MySQL 8, Active Storage | -| フロントエンド | React 19.1, Vite 6.3, TypeScript 5.8 | -| 通信・状態管理 | Axios, TanStack Query 5 | -| UI | Tailwind CSS 3, Framer Motion 12, Radix UI 系コンポーネント | -| Markdown | react-markdown, remark-gfm, react-markdown-editor-lite, 独自 Wiki 自動リンク | -| D&D | @dnd-kit/core | -| 画像処理 | MiniMagick | -| 外部取得 | Nokogiri, open-uri, Node screenshot script | -| 論理削除 | discard | -| PDF/差分等 | diff-lcs | -| ストレージ | development/test は Disk、production は Cloudflare R2 相当の S3 互換ストレージ | -| バッチ | Rake task、whenever schedule | +| バックエンド | Ruby 3.2.2 / Rails 8.0.2 API | +| DB | MySQL 8 / utf8mb4_0900_ai_ci | +| ファイル | Active Storage。Cloudflare R2/S3 利用を想定する構成あり | +| フロントエンド | React 19.1 / Vite 6.3 / TypeScript 5.8 | +| 通信 | Axios + TanStack Query | +| UI | Tailwind CSS / Framer Motion / shadcn 風 UI コンポーネント | +| Markdown | react-markdown / react-markdown-editor-lite / remark-wiki-autolink | +| バッチ | Rake task。Nico 同期、YouTube 同期、類似度計算など | -# 利用者と認証 +# 認証・ユーザ・BAN ## ユーザモデル -`users.role` は文字列 enum であり、現行ロールは次の 3 種である。 +`users` は次の主要属性を持つ。 -| ロール | 意味 | +| 属性 | 意味 | | --- | --- | -| guest | 初期発行ユーザ。閲覧中心 | -| member | 投稿・Wiki・タグ更新など通常の編集可 | -| admin | member 権限に加え、上位タグ設定など管理寄り操作可 | +| name | 表示名 | +| inheritance_code | 引継ぎコード。認証トークンとして使う | +| role | `guest` / `member` / `admin` | +| banned_at | BAN 時刻。NULL なら有効ユーザ | -`User#gte_member?` は `member` または `admin` を真とする。 +ロールは文字列 enum である。 + +| ロール | 権限の概略 | +| --- | --- | +| guest | 閲覧中心。編集系は不可 | +| member | 投稿・Wiki・タグ・素材など通常編集が可能 | +| admin | member 権限に加え、タグ親子関係など管理系操作が可能 | + +`User#gte_member?` は `member? || admin?` を返す。 ## 認証方式 -認証は ID / パスワードではなく、**引継ぎコード** による軽量認証である。 +認証は通常の ID / パスワードではなく、`inheritance_code` による軽量認証である。 -- ユーザ作成時に UUID 形式の `inheritance_code` を発行する。 -- フロントは `localStorage.user_code` に保持する。 -- API 呼び出し時、`X-Transfer-Code` ヘッダへ付与する。 -- `ApplicationController#authenticate_user` はヘッダ値から `User.find_by(inheritance_code:)` する。 - -この仕組みは簡単で UX は軽いが、通常のパスワード認証より防御面は薄い。公開段階では、権限範囲を絞る設計が必須である。 +- フロントは `localStorage.user_code` にコードを保持する。 +- API 呼び出し時、`X-Transfer-Code` ヘッダにコードを付与する。 +- サーバは `users.inheritance_code` と照合し、`current_user` を決定する。 ## 初回利用フロー -現行フロントの起動時処理は次の通り。 +フロント起動時の流れは次である。 -1. `localStorage.user_code` を読む。 -2. 存在する場合は `POST /users/verify` で検証する。 -3. 無効なら `POST /users` で guest ユーザを新規発行する。 -4. 存在しない場合も `POST /users` で guest ユーザを発行する。 -5. API レスポンスは camelCase に変換してフロント型へ流す。 +1. `localStorage.user_code` を確認する。 +2. ある場合は `POST /users/verify` へ送る。 +3. 有効なら返却ユーザを採用する。 +4. 無効またはコード無しなら `POST /users` で guest ユーザを新規作成する。 +5. 新規作成時、返却された引継ぎコードを localStorage に保存する。 + +## BAN 強制 + +過去仕様書との差分として重要。 + +現行 `ApplicationController` では、すべての API に対して次が `before_action` で走る。 + +1. `reject_banned_ip_address!` +2. `authenticate_user` +3. `reject_banned_user!` + +したがって現行実装では、**BAN は保存されているだけではなく、API レベルで強制される**。 + +| 対象 | 判定 | +| --- | --- | +| IP BAN | `ip_addresses.banned_at` が存在すれば 403 | +| User BAN | `users.banned_at` が存在すれば 403 | + +IP は `request.remote_ip` を `IPAddr#hton` で 16 byte binary 化し、`ip_addresses.ip_address` に保存する。 ## IP 紐づけ -`POST /users/verify` 実行時、サーバは `request.remote_ip` を 16 バイトのバイナリへ変換し、`ip_addresses` と `user_ips` に記録する。 +`POST /users` と `POST /users/verify` のタイミングで、ユーザと IP が紐づけられる。 -したがって、現行システムは **引継ぎコードと IP の対応を保持する土台** を持つ。ただし、`users.banned` と `ip_addresses.banned` はまだ認可処理へ強制接続されていない。 +- `ip_addresses`: IP アドレス本体。 +- `user_ips`: user と ip_address の複合主キー関係。 -## ユーザ設定画面 +注意点として、`request.remote_ip` が不正・空になるケースへの明示的な救済は薄い。ここはプロキシ構成・Cloudflare 経由・bot アクセスで事故り得る。 -現行 UI の `/users/settings` では次を扱う。 +# 主要ドメインモデル -- 表示名更新 -- 引継ぎコード表示 -- 引継ぎコード更新 -- 他ブラウザからの引継ぎ +## 投稿 posts -`settings` テーブルは存在するが、汎用設定の API・反映処理は未接続である。 +投稿は外部 URL を中心とするリンクデータである。 -# ドメインモデル - -## 投稿 `posts` - -投稿は URL を中心にしたリンクオブジェクトである。 - -| 属性 | 概要 | +| 属性 | 意味 | | --- | --- | -| `url` | 参照先 URL。一意。HTTP/HTTPS のみ | -| `title` | 表示用タイトル。NULL 可 | -| `thumbnail` | Active Storage 添付 | -| `thumbnail_base` | 外部サムネイル元 URL。NULL 可 | -| `parent_id` | 親投稿参照。現行では 1 親 | -| `uploaded_user_id` | 投稿者。同期投稿では NULL | -| `original_created_from` | 元コンテンツ作成日時の下限 | -| `original_created_before` | 元コンテンツ作成日時の上限(より前) | -| `created_at` / `updated_at` | 投稿レコードの作成・更新日時 | +| title | 表示タイトル | +| url | 投稿 URL。一意 | +| thumbnail_base | 外部サムネイル URL | +| uploaded_user_id | 手動投稿者。同期投稿は NULL になり得る | +| original_created_from | 元コンテンツ作成日時の下限 | +| original_created_before | 元コンテンツ作成日時の上限 | +| thumbnail | Active Storage 添付 | -### URL 正規化 +### URL 仕様 -保存前に次を行う。 +- URL は必須。 +- URL は一意。 +- HTTP / HTTPS のみ許可。 +- 保存前に前後空白を除去する。 +- host は小文字化される。 +- path 末尾の `/` は除去される。 -- 前後空白の除去 -- HTTP/HTTPS URL であることの検証 -- host の小文字化 -- path 末尾 `/` の除去 +### 元コンテンツ日時 -### オリジナル作成日時 +`original_created_from` と `original_created_before` の両方がある場合、`from < before` が必須である。 -`original_created_from` と `original_created_before` が両方ある場合、`from < before` が必須である。 +YouTube/Nico 同期では、動画公開時刻を分単位の範囲として保持する実装がある。 -この仕様は「日時そのもの」ではなく、「その範囲内にリンク先コンテンツが公開された」ことを表す。ニコニコ同期では分単位で `from` と `before = from + 1 minute` を生成する。 +### サムネイル -## 投稿版 `post_versions` +手動投稿時に `thumbnail` が渡されると Active Storage に添付され、`Post#resized_thumbnail!` により 180x180 の JPEG へリサイズされる。 -`post_versions` は投稿のスナップショット履歴である。 +## 投稿親子関係 post_implications -| 属性 | 概要 | +現行実装では、投稿の親子関係は `posts.parent_id` ではない。 + +`post_implications` により、投稿と親投稿は多対多で表現される。 + +| カラム | 意味 | | --- | --- | -| `post_id` | 対象投稿 | -| `version_no` | 投稿ごとの版番号。1 以上 | -| `event_type` | `create` / `update` / `discard` / `restore` | -| `title` | 当該版のタイトル | -| `url` | 当該版の URL | -| `thumbnail_base` | 当該版のサムネイル元 URL | -| `tags` | 当該版のタグ名スナップショット。空白区切り | -| `parent_id` | 当該版の親投稿 | -| `original_created_from` / `original_created_before` | 当該版の元コンテンツ作成日時範囲 | -| `created_at` | 版作成日時 | -| `created_by_user_id` | 版作成ユーザ。bot 操作は NULL | +| post_id | 子投稿 | +| parent_post_id | 親投稿 | -`PostVersion` は更新・削除不可の読み取り専用履歴として扱われる。`PostVersionRecorder` は投稿をロックし、直前版と同一スナップショットであれば `update` 版を増やさない。 +仕様: -### バックフィル仕様 +- 主キーは `(post_id, parent_post_id)`。 +- 自己参照は禁止。 +- 投稿作成・更新 API では `parent_post_ids` が必須。 +- `parent_post_ids` は空白区切りの ID 文字列として解釈される。 +- 存在しない親 ID が含まれると 422。 +- 自分自身を親にすると 422。 -`20260409123700_create_post_versions` では、既存の `post_tags` 履歴から投稿版を再構成する。 +この仕様は重要で、フロントや API クライアントは空でも `parent_post_ids` を送る必要がある。 -- 投稿ごとに `version_no = 1` の `create` 版を作る。 -- `post_tags.created_at` / `discarded_at` をイベントとして並べる。 -- 1 秒以内のタグ変更を同一バケットとしてまとめる。 -- 初回タグ付与が `posts.created_at + 1 second` 以内なら `create` 版へ統合する。 +## 投稿版 post_versions -これは過去履歴の近似復元であり、過去の投稿本文変更を完全に再現するものではない。 +投稿本体とタグ・親投稿構成のスナップショットは `post_versions` に保存される。 -## タグ `tags` とタグ名 `tag_names` +| 属性 | 意味 | +| --- | --- | +| post_id | 対象投稿 | +| version_no | 投稿内連番。1 以上 | +| event_type | create / update / discard / restore | +| title / url / thumbnail_base | 投稿本体スナップショット | +| tags | タグ名の空白区切りスナップショット | +| parent_post_ids | 親投稿 ID の空白区切りスナップショット | +| original_created_from / before | 元日時範囲 | +| created_by_user_id | 操作者 | -タグは 2 層構造である。 +`PostVersionRecorder` は更新前スナップショットを保証し、変更がなければ update 版を増やさない。 -- `tag_names`: 文字列としての名前。別名もここで扱う。 -- `tags`: カテゴリ・件数・実体としてのタグ。 +## タグ tags / tag_names -`tags.tag_name_id` は一意であり、実体タグは canonical な `tag_names` を参照する。 +タグ名文字列とタグ実体は分離されている。 + +- `tag_names`: 名前、別名関係、Wiki ページとの結合点。 +- `tags`: カテゴリ、投稿件数、タグ実体。 ### カテゴリ -| カテゴリ | 表示名 | 用途 | -| --- | --- | --- | -| `deerjikist` | ニジラー | 投稿者・人物・作者 | -| `meme` | 原作・ネタ元・ミーム等 | 元ネタ、文脈、シリーズ | -| `character` | キャラクター | キャラクタ | -| `general` | 一般 | 通常タグ | -| `material` | 素材 | 素材タグ | -| `meta` | メタタグ | 運用・状態タグ | -| `nico` | ニコニコタグ | 外部ニコニコタグ | +現行カテゴリは次である。 + +| category | 用途 | +| --- | --- | +| deerjikist | ニジラー | +| meme | ミーム・原作・ネタ元など | +| character | キャラクター | +| general | 一般 | +| material | 素材 | +| nico | ニコニコ外部タグ | +| meta | メタタグ | ### 別名 -`tag_names.canonical_id` が NULL のものは実体名、非 NULL のものは別名である。 - -制約は次の通り。 +`tag_names.canonical_id` により別名を表現する。 +- `canonical_id = NULL`: 正規名。 +- `canonical_id != NULL`: 別名。 - 別名の参照先も別名であってはならない。 -- 別名名には `:` を含められない。 -- タグまたは Wiki を持つ `tag_name` は別名化できない。 -- 検索・投稿正規化・補完では canonical 名へ解決する。 -- `/tags/name/:name` は exact match であり、別名解決 API ではない。 +- 別名名に `:` を含められない。 +- タグまたは Wiki ページを持つ tag_name は別名化できない。 -### タグ名サニタイズ +`TagName.canonicalise` は、既知の別名を正規名へ置換する。 -`tag_name_sanitisation_rules` は、タグ名を正規表現置換する内部ルールである。 +## タグサニタイズ tag_name_sanitisation_rules -- 主キーは `priority`。 -- `source_pattern` は正規表現。 -- `replacement` で置換する。 -- `TagName` バリデーションで `sanitise(name) == name` を要求する。 -- `apply!` は既存 `TagName` を一括変換し、衝突時はタグ統合または `TagName` 差し替えを行う。 +タグ名には優先度付きのサニタイズ規則がある。 -この機能は UI には出ていない。鯖缶向けの内部運用層である。 +| 属性 | 意味 | +| --- | --- | +| priority | 主キー。適用順序 | +| source_pattern | 正規表現 | +| replacement | 置換後文字列 | +| discarded_at | 論理削除 | + +`TagName` のバリデーションは、名前が `TagNameSanitisationRule.sanitise(name)` と一致することを要求する。 + +したがって、サニタイズ規則は「保存前に自動で直す」だけではなく、「規則に反する名前を拒否する」層でもある。 + +## タグ正規化 + +投稿作成・更新時、タグ入力は `Tag.normalise_tags!` で正規化される。 + +### カテゴリプレフィクス + +| 入力接頭辞 | category | +| --- | --- | +| general: / gen: | general | +| deerjikist: / djk: | deerjikist | +| meme: | meme | +| character: / chr: | character | +| material: / mtr: | material | +| meta: | meta | + +`nico:` は通常手入力では拒否される。 ### 自動付与タグ -`Tag.normalise_tags` は投稿作成・更新時に次を行う。 +通常の正規化では次が自動付与される。 | 条件 | 自動付与 | | --- | --- | -| `with_tagme: true` かつタグ数が 10 未満 | `タグ希望` | -| ニジラー系タグが 1 件もない | `ニジラー情報不詳` | +| タグ数が 10 未満 | タグ希望 | +| deerjikist カテゴリが無い | ニジラー情報不詳 | -投稿作成時は `with_tagme: true`。投稿更新時は `with_tagme: false` なので、更新時には `タグ希望` は自動付与されない。 +投稿更新時は `with_tagme: false` のため、更新時に「タグ希望」を新規補完しない。 -### ニコニコタグの扱い +### 親タグ展開 -通常投稿のタグ入力で `nico:` プレフィクスが含まれた場合、`Tag::NicoTagNormalisationError` が発生し、リクエストは `400 Bad Request` になる。 +`Tag.expand_parent_tags` により、入力タグの親タグを再帰的に追加する。 -`nico` カテゴリは手入力で直接追加するものではなく、ニコニコ同期・ニコタグ連携経路で扱う。 +結果として投稿には、明示タグだけではなく、親カテゴリ的なタグも保存される。 -## 投稿とタグの関係 `post_tags` +## 投稿タグ post_tags -`post_tags` は投稿とタグの多対多関係であり、同時にタグ付与履歴の基礎である。 +投稿とタグは多対多である。 -- `discarded_at` による論理削除。 -- `created_user_id` と `deleted_user_id` を保持。 -- 有効な `(post_id, tag_id)` は一意。 -- `PostTag#discard_by!` は `tags.post_count` を減算する。 -- 物理削除は禁止される。 +| 属性 | 意味 | +| --- | --- | +| post_id | 投稿 | +| tag_id | タグ | +| created_user_id | 付与者 | +| deleted_user_id | 削除者 | +| discarded_at | 論理削除時刻 | +| active_unique_key | 有効レコード一意制約用の生成列 | -現在の履歴表示の主役は `post_versions` だが、`post_tags` にもタグ単位のイベント履歴が残る。 +現行有効な `(post_id, tag_id)` の重複は DB レベルで防がれている。 -## 上位タグ `tag_implications` +`PostTag` は論理削除されるため、タグ付与履歴としても使われる。 -`tag_implications` は子タグと親タグの関係である。 +## タグ親子 tag_implications -- `tag_id`: 子タグ -- `parent_tag_id`: 親タグ -- `(tag_id, parent_tag_id)` は一意 -- 自己参照は禁止 +タグの親子関係は `tag_implications` で表す。 -投稿保存時には `Tag.expand_parent_tags` により、指定タグの親タグが再帰的に自動付与される。 +| カラム | 意味 | +| --- | --- | +| tag_id | 子タグ | +| parent_tag_id | 親タグ | -現行 UI では、投稿詳細サイドバーでタグを D&D することにより、上位タグ設定・解除・カテゴリ変更を行える。成功には admin 権限が必要である。 +仕様: -## ニコタグ連携 `nico_tag_relations` +- 同一組み合わせは一意。 +- admin のみ API で作成・削除できる。 +- nico カテゴリタグは親子操作不可。 +- 投稿タグ保存時、親タグは自動展開される。 -`nico_tag_relations` は外部ニコニコタグと内部タグの橋渡しである。 +## タグ版 tag_versions / nico_tag_versions -- `nico_tag_id` は `nico` カテゴリ必須。 -- `tag_id` は `nico` カテゴリ禁止。 -- `GET /tags/nico` でニコタグと連携先タグを取得する。 -- `PUT /tags/nico/:id` で連携先タグを更新する。 +通常タグと nico タグは別テーブルで履歴を持つ。 -ニコニコ同期では、新しく付いたニコタグに対してだけ、既存の連携先内部タグを自動付与する。 +### tag_versions -## ニジラー `deerjikists` +| 属性 | 意味 | +| --- | --- | +| tag_id | 対象タグ | +| version_no | タグ内連番 | +| event_type | create / update / discard / restore | +| name | 名前スナップショット | +| category | カテゴリ | +| aliases | 別名リスト | +| parent_tag_ids | 親タグ ID リスト | +| created_by_user_id | 操作者 | -`deerjikists` は外部プラットフォーム上の人物識別子と内部タグを対応させる。 +### nico_tag_versions -- 主キーは `(platform, code)` の複合主キー。 -- `platform` は `nico` / `youtube`。 -- 対応先タグは `deerjikist` カテゴリ必須。 +| 属性 | 意味 | +| --- | --- | +| tag_id | nico タグ | +| version_no | nico タグ内連番 | +| event_type | create / update / discard / restore | +| name | nico タグ名 | +| linked_tags | 連携内部タグ名 | +| created_by_user_id | 操作者 | -ニコニコ同期では、投稿者コードに対応する `deerjikist` があれば、そのタグを自動付与する。存在しない場合、既存内部タグにニジラー系タグが無ければ `ニジラー情報不詳` を付与する。 +## ニコタグ連携 nico_tag_relations + +`nico_tag_relations` は nico カテゴリタグと内部タグを結ぶ。 + +- `nico_tag_id`: nico カテゴリ必須。 +- `tag_id`: nico カテゴリ禁止。 + +これは外部ニコニコタグを内部タグ体系へ変換するための関係である。 + +## ニジラー deerjikists + +外部プラットフォーム上の人物識別子と内部タグを結ぶ。 + +| 属性 | 意味 | +| --- | --- | +| platform | nico / youtube | +| code | 外部識別子 | +| tag_id | deerjikist カテゴリのタグ | + +主キーは `(platform, code)`。 + +YouTube の `@handle` は、更新 API 内でチャンネル ID `UC...` へ正規化を試みる。 + +## 素材 materials + +現行実装で追加されている重要機能。 + +素材は、素材タグまたはキャラクタータグに紐づくファイル・参考 URL である。 + +重要なのは、`character` と `material` は単純な上下カテゴリではない点である。現行運用では、`character` タグを素材集合の代表タグとし、関連する `material` タグを `TagImplication` によって子タグとしてぶら下げる。つまり、キャラクタータグは「そのキャラクターに関する素材タグ群の入口」として機能する。 + +タグ分けするほどではないが異なる素材差分については、将来的に **包摂素材** として単一 Material の配下へ複数保持できるようにする計画である。これは、立ち絵差分・音声差分・参考画像などを何でもタグ化してタグ空間を肥大化させるのを避けるための拡張である。 + +![素材タグ運用: 現行の親子タグ + 将来の包摂素材](btrc_spec_figures/material_model.png) + +| 属性 | 意味 | +| --- | --- | +| url | 参考 URL。論理削除されていない場合は一意 | +| parent_id | 親素材。将来的な包摂素材表現の土台になる | +| tag_id | 対応タグ。素材 1 件につき一意 | +| created_by_user_id | 作成者 | +| updated_by_user_id | 更新者 | +| discarded_at | 論理削除 | +| file | Active Storage 添付 | + +### 制約 + +- `tag_id` は必須かつ一意。 +- タグは `material` または `character` カテゴリのみ許可。 +- `character` は素材集合の代表タグとして許可される。 +- `material` は具体的な素材タグとして許可される。 +- `url` または `file` の少なくとも一方が必須。 +- `discarded_at` が NULL の場合だけ `url` 一意制約が効く。 + +### 現行運用上の意味 + +- キャラクターに関連する複数素材は、現時点では `TagImplication` によって `character` 親タグと `material` 子タグに分解して扱う。 +- そのため、`materials.tag_id` が一意であっても、素材をタグ単位へ分ける限りは大きな問題は起きにくい。 +- ただし、タグを増やすほどではない素材差分を複数持ちたい場合は、現行設計だけでは表現力が足りない。 +- この不足分を埋める将来仕様が **包摂素材** である。 + +### バージョン + +`material_versions` が存在し、素材の URL・親素材・タグ・作成/更新者・discard 状態を履歴化する構造がある。 + +ただし、コントローラ上は素材の version API は確認できない。モデル・サービス側の実運用接続は追加確認が必要。 ## Wiki -Wiki は、ページ本体と改訂履歴を DB 内で持つ版管理システムである。 +Wiki はタグ名に紐づく説明ページである。 -### `wiki_pages` +### wiki_pages -- `tag_name_id` がタイトルに対応する。 -- 1 タイトルにつき 1 ページ。 -- `created_user_id` / `updated_user_id` を持つ。 -- `next_asset_no` を持つが、現行の画像アップロード UI/API は未接続。 - -### `wiki_revisions` - -- `kind` は `content` または `redirect`。 -- `base_revision_id` で改訂の親を持つ。 -- `message` を持てる。 -- `lines_count` と `tree_sha256` を持つ。 -- `redirect` の場合は `redirect_page_id` を持つ。 - -### 行ストレージ - -- 本文は `wiki_revision_lines` を介して行単位で構成される。 -- 行本文は `wiki_lines` に SHA-256 付きで保存される。 -- 同じ行は重複排除される。 - -これは単なる Markdown 本文 1 カラム保存ではない。DB 上に Git 風の行ストアを持つ構造である。 - -## Wiki 画像 `wiki_assets` - -`wiki_assets` テーブルは存在する。 - -| 属性 | 概要 | +| 属性 | 意味 | | --- | --- | -| `wiki_page_id` | 対象ページ | -| `no` | ページ内画像番号 | -| `alt_text` | 代替テキスト | -| `sha256` | ファイル重複判定用 SHA-256 | -| `created_by_user_id` | 作成者 | +| tag_name_id | Wiki タイトルに対応する tag_name | +| body | 現行本文キャッシュ | +| created_user_id | 作成者 | +| updated_user_id | 更新者 | +| next_asset_no | Wiki 画像/添付素材の採番用 | +| discarded_at | 論理削除 | -ただし、2026-04-19 時点では `WikiAsset` モデル、controller、route、フロント実装が確認できない。したがって **スキーマ準備済み、機能未接続** と扱う。 +`wiki_pages.tag_name_id` は一意。 -2026-04-20 の開発者ヒアリングでは、Wiki 画像は通常 URL ではなく **専用記法** で扱う方針になった。候補は `![代替テキスト](asset:1)` のように、ページ内画像番号または asset 識別子を Markdown の URL 部へ入れる方式である。実装時は、Markdown レンダラ側で `asset:` スキームを検出し、対象ページの `wiki_assets.no` から Active Storage URL へ解決する。 +### wiki_revisions / wiki_lines -## 素材 `materials` +Wiki は改訂と行ストアを分離する。 -`materials` は素材集の実体である。 +- `wiki_revisions`: 改訂メタ情報。 +- `wiki_revision_lines`: 改訂内の行順序。 +- `wiki_lines`: 行本文を SHA-256 で重複排除保存。 -| 属性 | 概要 | -| --- | --- | -| `url` | 参考 URL。NULL 可 | -| `file` | Active Storage 添付。画像・動画・音声を主に想定 | -| `tag_id` | 対応タグ。現行モデルでは一意 | -| `parent_id` | 親素材 | -| `created_by_user_id` / `updated_by_user_id` | 作成・更新ユーザ | -| `discarded_at` | 論理削除 | -| `active_url` | 未削除時のみ URL を返す生成列。一意制約あり | +`wiki_revisions.kind` は現行 enum で `content` / `redirect` を持つ。ただし `Wiki::Commit#redirect!` は現在 `廃止しました.` として例外を投げる。 -`Material` は `url` または `file` のどちらかを必須とする。タグは `character` または `material` カテゴリを許容する。これは単なる実装の緩さではなく、`character` を「下位区分としての material を包括する代表素材タグ」として扱う設計判断である。したがって、素材タグとしては `material` が基本だが、代表素材・キャラクタ素材の受け皿として `character` も許容する。 +### wiki_versions -`material_versions` テーブルは存在するが、モデル・履歴記録・API・画面は未接続である。 +`wiki_versions` も存在し、Wiki ページの版管理スナップショットを保持する。 -## 閲覧済 `user_post_views` +現行 `Wiki::Commit.content!` は `WikiVersionRecorder.record!` を呼ぶため、行単位 revision と別に、通常の version 履歴も記録される。 -ユーザ単位で投稿の閲覧済状態を管理する。 +### Wiki 競合制御 + +`Wiki::Commit.content!` は `base_revision_id` を受け取る。指定があり、現在の最大 revision id と一致しない場合は `Wiki::Commit::Conflict` を投げ、コントローラは 409 を返す。 + +現行 `WikiPagesController#update` は `params[:base_revision_id]` を使う。したがって、過去仕様書にあった「クライアント送信値を使わないため競合検出が活きていない」という記述は、現行実装では修正済みである。 + +## 閲覧済 user_post_views + +ユーザごとの投稿閲覧済み状態を保持する。 - 主キーは `(user_id, post_id)`。 -- 投稿詳細画面でボタンにより付与・解除できる。 -- 自動既読化の設定は将来構想としてあるが、現行は未接続。 +- `POST /posts/:id/viewed` で付与。 +- `DELETE /posts/:id/viewed` で解除。 -## 類似度 `post_similarities` / `tag_similarities` +## 類似度 post_similarities / tag_similarities -投稿・タグには類似度の事前計算テーブルがある。 +投稿類似度とタグ類似度は事前計算テーブルで保持される。 -- `post_similarities`: 投稿に付いたタグ集合から投稿同士の cosine 類似度を計算。 -- `tag_similarities`: タグに属する投稿集合からタグ同士の cosine 類似度を計算。 -- それぞれ上位 20 件を保存する。 - -投稿詳細では `post.related(limit: 20)` により関連投稿を返す。タグ類似度については、テーブル・モデル・タスクはあるが、現行 UI での主導線は薄い。 - -## 上映会 `theatres` - -Theatre は共同視聴用の会場ドメインである。 - -| テーブル | 役割 | -| --- | --- | -| `theatres` | 会場、現在上映中投稿、ホスト、期間 | -| `theatre_comments` | 会場コメント。主キーは `(theatre_id, no)` | -| `theatre_watching_users` | 在席ユーザ。`expires_at` で 30 秒 TTL | - -現行では会場一覧・作成・編集は存在しない。トップメニューは固定で `/theatres/1` を指す。 - -# 現行機能仕様 - -## 投稿一覧・検索 - -### API - -`GET /posts` - -### パラメータ - -| パラメータ | 意味 | -| --- | --- | -| `tags` | 空白区切りタグ列 | -| `match` | `all` または `any` | -| `title` | タイトル部分一致 | -| `url` | URL 部分一致 | -| `original_created_from` / `original_created_to` | 元コンテンツ作成日時範囲 | -| `created_from` / `created_to` | 投稿作成日時範囲 | -| `updated_from` / `updated_to` | 投稿更新日時範囲 | -| `page` / `limit` | ページング | -| `order` | `field:direction` | - -`order` の field は次の 5 種である。 - -- `title` -- `url` -- `original_created_at` -- `created_at` -- `updated_at` - -`title` と `url` は direction 未指定時に昇順、それ以外は降順である。 - -### タグ検索 - -タグ検索は次の意味を持つ。 - -| 指定 | 意味 | -| --- | --- | -| `match=all` または未指定 | 各タグを AND 的に絞り込む | -| `match=any` | 各タグ条件を OR 的に結合する | -| `not:タグ名` | 否定条件 | -| 別名タグ | 検索前に canonical 名へ解決される | - -`match=any` では否定条件も OR の一部として扱われる。つまり `A not:B` は「A が付いている、または B が付いていない」に近い挙動になる。ここは直感とずれやすい。 - -### 更新日時 - -一覧返却の `updated_at` は、単純な `posts.updated_at` ではない。 - -サーバは次の大きい方を `updated_at_all` として返し、並び替えにも使う。 - -- `posts.updated_at` -- 対象投稿に紐づく `post_tags.updated_at` の最大値 - -タグ変更も投稿更新として扱う設計である。 - -### 返却 - -返却は `{ posts, count }`。各投稿にはタグ・投稿者・サムネイル URL が含まれる。 - -## ランダム投稿 - -### API - -`GET /posts/random` - -`tags` と `match` を受け取り、絞り込み結果から `RAND()` で 1 件返す。 - -## 投稿詳細 - -### API - -`GET /posts/:id` - -### 返却 - -- 投稿基本情報 -- タグツリー -- 関連投稿 最大 20 件 -- 閲覧済フラグ - -タグは `tag_implications` に基づきツリー化される。ループがあっても再帰停止する。 - -### 画面 - -投稿詳細画面では次が表示される。 - -- 埋め込み表示 -- タグツリー -- 投稿情報サイドバー -- 関連投稿 -- 編集タブ -- 閲覧済トグル -- 履歴リンク - -投稿詳細サイドバーでは、D&D により上位タグ設定・解除・カテゴリ変更を試みる。API 側で admin 権限が要求されるため、member / guest は失敗する。 - -## 投稿埋め込み - -`PostEmbed` の現行専用対応は次の通り。 - -| サイト | 条件 | 表示 | +| テーブル | 単位 | 特徴量 | | --- | --- | --- | -| ニコニコ動画 | `nicovideo.jp/watch/:id` | `NicoViewer` | -| YouTube | `youtube.com` かつ `v` パラメータあり | `react-youtube` | -| X / Twitter | `x.com` または `twitter.com` の `/status/:id` | `TwitterEmbed` | -| その他 | ユーザ確認後 | `iframe` | +| post_similarities | 投稿 → 関連投稿 | 投稿に付いたタグ集合 | +| tag_similarities | タグ → 関連タグ | タグに属する投稿集合 | -`youtu.be`、Pixiv、Bluesky、Tiktok、bilibili、ニジカ投稿局などは専用埋め込み未実装である。 +上位 20 件のみ保存する設計。 -## 新規投稿 +## 上映会 theatres -### API +上映会は共同視聴機能である。 -`POST /posts` - -### 権限 - -member 以上。 - -### 入力 - -- `title` -- `url` -- `thumbnail` -- `tags` -- `original_created_from` -- `original_created_before` - -### 保存フロー - -1. 投稿レコード作成。 -2. サムネイル添付。 -3. タグ名正規化。 -4. 親タグ展開。 -5. `post_tags` 同期。 -6. サムネイルを 180x180 へリサイズ。 -7. `post_versions` に `create` 版を記録。 - -`nico:` タグが含まれる場合は `400 Bad Request`。 - -## 投稿編集 - -### API - -`PUT /posts/:id` - -### 権限 - -member 以上。 - -### 更新対象 - -- `title` -- `tags` -- `original_created_from` -- `original_created_before` - -URL とサムネイルの通常編集は現行 API では扱っていない。 - -既存の `nico` カテゴリタグは維持され、一般タグ側だけが再正規化される。その後、親タグ展開と `post_tags` 同期を行い、差分があれば `post_versions` に `update` 版を記録する。 - -### 排他制御 - -現行 API には投稿編集の排他制御がない。issue では `updated_at` をリクエストに含め、差があれば 409 を返す構想があるが、未実装である。 - -## 閲覧済 - -| API | 意味 | +| 属性 | 意味 | | --- | --- | -| `POST /posts/:id/viewed` | 閲覧済にする | -| `DELETE /posts/:id/viewed` | 未閲覧に戻す | +| name | 会場名 | +| opens_at / closes_at | 開始/終了時刻 | +| kind | 種別。現行 UI では薄い | +| current_post_id | 現在上映中投稿 | +| current_post_started_at | 現在投稿の開始時刻 | +| next_comment_no | コメント採番 | +| host_user_id | 現在ホスト | +| created_by_user_id | 作成者 | +| discarded_at | 論理削除 | -認証済ユーザなら操作可能である。 +# API 仕様 -## 投稿版履歴 +## 投稿 API -### API +### GET /posts -`GET /posts/versions` +投稿一覧を取得する。 -### パラメータ +#### パラメータ | パラメータ | 意味 | | --- | --- | -| `post` | 投稿 ID で絞り込み | -| `tag` | タグ ID で絞り込み | -| `page` / `limit` | ページング | +| tags | 空白区切りタグ検索 | +| match | `all` 相当または `any` | +| title | タイトル部分一致 | +| url | URL 部分一致 | +| original_created_from / original_created_to | 元コンテンツ作成日時範囲 | +| created_from / created_to | 投稿作成日時範囲 | +| updated_from / updated_to | 投稿またはタグ更新日時範囲 | +| page | ページ。既定 1 | +| limit | 件数。既定 20 | +| order | `field:direction` | -タグで絞り込む場合、現在版または直前版のタグスナップショットに対象タグ名が含まれる版を返す。これにより、タグが追加された版だけでなく、削除された版も拾える。 +`order` の field は次。 -### 返却 +- title +- url +- original_created_at +- created_at +- updated_at -返却は `{ versions, count }`。 +`updated_at` は `posts.updated_at` と `post_tags.updated_at` の最大値を `updated_at_all` として使う。 -各 version は次を含む。 +### タグ検索意味論 -- `post_id` -- `version_no` -- `event_type` -- `title.current` / `title.prev` -- `url.current` / `url.prev` -- `thumbnail.current` / `thumbnail.prev` -- `thumbnail_base.current` / `thumbnail_base.prev` -- `tags[]`: `context` / `added` / `removed` -- `original_created_from.current` / `.prev` -- `original_created_before.current` / `.prev` -- `created_at` -- `created_by_user` +`tags` は空白区切り。各要素は `TagName.canonicalise` により別名解決される。 -現行では `thumbnail.current` / `prev` は常に NULL で、サムネイル表示は `thumbnail_base` に依存する。 +- `match=any`: OR。 +- それ以外: AND。 +- `not:` 接頭辞: 否定条件。 -### 画面 +注意: `match=any` で `not:` を混ぜると、SQL 的には「否定条件を OR する」ため、期待とズレる可能性がある。仕様として許すか、UI で制限するか要確認。 -フロントの `/posts/changes` は、内部的には `GET /posts/versions` を呼ぶ。つまり画面名は「耕作履歴」だが、主役データはタグイベントではなく投稿版スナップショットである。 +### GET /posts/random -### 復元 +現在の `tags` / `match` 条件に合う投稿をランダムに 1 件返す。 -履歴画面の「復元」は専用の restore API ではない。選択版の `title`、`tags`、`original_created_from`、`original_created_before` を `PUT /posts/:id` へ送る。 +### GET /posts/:id -そのため、以下の制限がある。 +投稿詳細を取得する。 -- `url` は復元されない。 -- `thumbnail_base` は現時点では復元されない。将来的に入力欄を追加した段階で復元対象へ含める方針である。 -- `event_type = restore` の版は現行フローでは作られない。 -- `nico:` タグは復元送信時に除外される。 +返却には次を含む。 -2026-04-20 の開発者ヒアリングでは、`thumbnail_base` は将来的に復元対象へ含める方針とされた。ただし現行フロントの編集入力欄に出てこない項目であるため、現段階では復元しない。これは「履歴として保持しない」のではなく、「編集 UI が責任を持てる項目だけ戻す」という暫定仕様である。 +- 投稿基本情報。 +- タグ木構造。 +- 関連投稿最大 20 件。 +- 現在ユーザの閲覧済みフラグ。 +- 親投稿・子投稿・兄弟投稿情報。 -## 旧タグ変更履歴 +### POST /posts -### API +投稿を作成する。member 以上必須。 -`GET /posts/changes` +入力: -この API は `post_tags` から `add` / `remove` のイベント列を生成する。現行画面の主導線ではなくなっているが、API としては残っている。 - -`/posts/versions` と `/posts/changes` は意味が違う。2026-04-20 の開発者ヒアリングでは、`/posts/changes` は将来的に廃止する可能性が高いと整理された。現時点では後方互換上残っている旧 API として扱う。 - -# タグ機能 - -## タグ一覧・検索 - -### API - -`GET /tags` - -### パラメータ - -| パラメータ | 意味 | -| --- | --- | -| `post` | 特定投稿に付いたタグだけへ絞る | -| `name` | 名前部分一致 | -| `category` | カテゴリ一致 | -| `post_count_gte` / `post_count_lte` | 投稿件数範囲 | -| `created_from` / `created_to` | 作成日時範囲 | -| `updated_from` / `updated_to` | 更新日時範囲 | -| `page` / `limit` | ページング | -| `order` | `field:direction` | - -`order` の field は次の 5 種である。 - -- `name` -- `category` -- `post_count` -- `created_at` -- `updated_at` - -カテゴリ並びは独自順序である。 - -1. `deerjikist` -2. `meme` -3. `character` -4. `general` -5. `material` -6. `meta` -7. `nico` - -## タグ補完 - -### API - -`GET /tags/autocomplete` - -### パラメータ - -| パラメータ | デフォルト | 意味 | +| パラメータ | 必須 | 意味 | | --- | --- | --- | -| `q` | なし | 検索文字列 | -| `nico` | `true` | `nico:` 側候補を含める | -| `present` | `true` | `post_count > 0` のタグに絞る | +| title | 任意 | タイトル | +| url | 必須 | URL | +| thumbnail | 任意 | サムネイルファイル | +| tags | 任意 | 空白区切りタグ | +| original_created_from | 任意 | 元日時下限 | +| original_created_before | 任意 | 元日時上限 | +| parent_post_ids | 必須 | 空白区切り親投稿 ID | -### 挙動 +保存フロー: -- `q` 先頭の `not:` は除去して補完する。 -- canonical 名の前方一致を拾う。 -- 別名の前方一致も拾い、canonical 側のタグを返す。 -- 別名ヒット時は `matched_alias` を付ける。 -- 並びは `post_count DESC, tag_names.name`。 +1. 投稿を保存。 +2. タグを正規化。 +3. 関連タグのバージョンスナップショットを確保。 +4. 親タグを展開。 +5. `post_tags` を同期。 +6. 親投稿関係を同期。 +7. サムネイルをリサイズ。 +8. `post_versions` に create 版を記録。 + +### PUT /posts/:id + +投稿を更新する。member 以上必須。 + +更新対象: + +- title +- original_created_from +- original_created_before +- tags +- parent_post_ids + +注意: + +- URL の更新は現行 API では行わない。 +- nico カテゴリタグは既存分を維持したまま、手入力側タグを再計算する。 +- `parent_post_ids` は更新時も必須。 +- 投稿の optimistic locking 用 `version_no` は posts テーブルにまだ無い。履歴はあるが、投稿更新 API で base version を照合する仕様は未実装。 + +### POST /posts/:id/viewed / DELETE /posts/:id/viewed + +ログイン済みユーザの閲覧済み状態を付与・解除する。 + +### GET /posts/changes + +投稿タグ付与履歴を取得する。 + +| パラメータ | 意味 | +| --- | --- | +| id | 投稿 ID 絞り込み | +| tag | タグ ID 絞り込み | +| page / limit | ページング | + +返却されるのは、投稿本体履歴ではなく `post_tags` の add/remove イベントである。 + +### GET /posts/versions + +投稿本体スナップショット履歴を取得する。 + +| パラメータ | 意味 | +| --- | --- | +| post | 投稿 ID 絞り込み | +| tag | タグ ID 絞り込み | +| page / limit | ページング | + +前版との差分として、title / url / thumbnail_base / tags / parent_post_ids / original_created_from / original_created_before などを返す。 + +## タグ API + +### GET /tags + +タグ一覧・検索を取得する。 + +| パラメータ | 意味 | +| --- | --- | +| post | 指定投稿に付いたタグだけに絞る | +| name | 名前部分一致 | +| category | カテゴリ一致 | +| post_count_gte / post_count_lte | 投稿件数範囲 | +| created_from / created_to | 作成日時範囲 | +| updated_from / updated_to | 更新日時範囲 | +| page / limit | ページング | +| order | `field:direction` | + +`order` の field は name / category / post_count / created_at / updated_at。 + +category 並びは独自順序。 + +1. deerjikist +2. meme +3. character +4. general +5. material +6. meta +7. nico + +注意: `count` は `q.size` を返しており、ページング後/前の扱いが ActiveRecord の状態に依存しうる。厳密な総件数仕様としては弱い。 + +### GET /tags/with-depth + +階層表示用タグを取得する。 + +| パラメータ | 意味 | +| --- | --- | +| parent | 親タグ ID。無ければルートタグ | + +対象カテゴリは `meme` / `character` / `material` に限定される。 + +返却タグには `has_children` が付く。 + +### GET /tags/autocomplete + +タグ補完。 + +| パラメータ | 既定 | 意味 | +| --- | --- | --- | +| q | 空 | 前方一致検索語 | +| nico | true | nico 候補を含める | +| present | true | post_count > 0 に限定 | + +挙動: + +- `q` 先頭の `not:` は除去する。 +- canonical 名前方一致を検索する。 +- 別名前方一致も拾い、正規タグを返す。 +- 別名ヒット時は `matched_alias` を返す。 - 最大 20 件。 -## タグ詳細 +### GET /tags/:id / GET /tags/name/:name -| API | 意味 | -| --- | --- | -| `GET /tags/:id` | ID でタグ取得 | -| `GET /tags/name/:name` | 名前完全一致でタグ取得 | -| `GET /tags/:id/deerjikists` | タグに紐づくニジラー一覧 | -| `GET /tags/name/:name/deerjikists` | 名前指定版 | -| `GET /tags/name/:name/materials` | 素材ツリー取得 | +タグ詳細を取得する。 -`/tags/name/:name` は exact match であり、別名解決をしない。 +`/tags/name/:name` は exact name であり、別名から正規タグへ自動解決しない。 -## タグ階層取得 +### PUT /tags/:id -### API +タグ全体更新。member 以上必須。 -`GET /tags/with-depth` +現行実装では以下を受ける。 -### パラメータ +- name +- category +- aliases +- parents + +仕様: + +- nico タグの編集は禁止。 +- `nico` カテゴリへの変更は禁止。 +- 特殊タグの改名は禁止。 +- name 変更時は、対応 Wiki があれば Wiki 版も更新記録される。 +- aliases 更新時は対象・影響タグの snapshot を確保する。 +- parents 更新時は親タグを再正規化し、既存親関係を全置換する。 + +### PATCH /tags/:id + +狭いタグ更新。member 以上必須。詳細は `update_all` と分担があり、実装確認継続対象。 + +### GET /tags/versions + +タグ履歴を取得する。 | パラメータ | 意味 | | --- | --- | -| `parent` | 親タグ ID。未指定ならルートタグを返す | +| id | タグ ID 絞り込み | +| page / limit | ページング | -### 返却 +前版との差分として、name / category / aliases / parent_tags を返す。 -対象カテゴリは `meme` / `character` / `material`。各タグに `has_children` と `children: []` を付けて返す。 +### POST /tags/:parent_id/children/:child_id -現行では素材サイドバーで使用される。`meme` は子を持つものだけ表示される。 +タグ親子関係を追加する。admin のみ。 -## タグ更新 +- nico タグは不可。 +- 子タグの snapshot を確保してから関係追加。 +- tag_versions に update を記録。 -### API +### DELETE /tags/:parent_id/children/:child_id -`PUT /tags/:id` +タグ親子関係を削除する。admin のみ。 -### 権限 +## ニコタグ API -member 以上。 +### GET /tags/nico -### 更新対象 - -- `name` -- `category` - -タグ名変更は `tag.tag_name.update!`、カテゴリ変更は `tag.update!` で行われる。 - -## 上位タグ管理 - -| API | 意味 | 権限 | -| --- | --- | --- | -| `POST /tags/:parent_id/children/:child_id` | child を parent の子にする | admin | -| `DELETE /tags/:parent_id/children/:child_id` | child を parent の子から外す | admin | - -現行フロントでは、投稿詳細サイドバーの D&D 操作からこれらを叩く。 - -ただし、専用の「上位タグ設定画面」は未実装である。 - -## 別名タグ管理 - -モデル・検索・補完・正規化の土台はあるが、別名の作成・解除・一覧管理 UI/API は未実装である。 - -issue では「管理者用別名タグ作成画面」が残っている。公開前にここはかなり重要である。 - -# ニコニコ連携 - -## ニコタグ一覧 - -### API - -`GET /tags/nico` - -### 挙動 - -- `Tag.nico_tags` を `updated_at DESC` で取得する。 -- `cursor` に ISO8601 時刻を渡すと、その時刻より古いものを取得する。 -- `limit` デフォルトは 20。 -- 各ニコタグに `linked_tags` を付与して返す。 - -## ニコタグ更新 - -### API - -`PUT /tags/nico/:id` - -### 権限 - -member 以上。 - -### 挙動 - -- 対象タグは `nico` カテゴリ必須。 -- 入力 `tags` を通常タグとして再正規化する。 -- 連携先に `nico` カテゴリは指定不可。 -- 成功時、連携先タグ配列を返す。 - -## 日次同期 `nico:sync` - -現行タスクは外部 Python スクリプト `get_videos.py` を呼び、ニコニコ動画情報を取り込む。 - -### 主な流れ - -1. `MYSQL_USER`、`MYSQL_PASS`、`NIZIKA_NICO_PATH` を環境変数から読む。 -2. `python3 #{NIZIKA_NICO_PATH}/get_videos.py` を実行する。 -3. 既存投稿は `nicovideo.jp/watch/:code` の正規表現で検索する。 -4. タイトル、元投稿日、サムネイルを更新する。 -5. 新規投稿なら `タグ希望`、`bot操作`、`ニコニコ`、`動画` を初期付与する。 -6. 外部タグを `nico:` 付き内部タグへ変換する。 -7. 新しく追加されたニコタグについてだけ、連携済内部タグを展開する。 -8. deerjikist 対応があればニジラータグを付与する。 -9. 必要に応じて `bot操作` を追加する。 -10. 作成・変更があれば `PostVersionRecorder` で投稿版を記録する。 - -これは単なるミラーではなく、外部タグを内部知識へ変換する ETL である。 - -# Wiki 機能 - -## 一覧・検索 - -| API | 意味 | -| --- | --- | -| `GET /wiki` | Wiki 一覧またはタイトル検索 | -| `GET /wiki/search` | `index` への委譲 | - -`title` 未指定なら全件、指定時は `tag_names.name LIKE %title%` で最大 20 件を返す。 - -フロントの `/wiki` は検索画面であり、本文検索欄に見える UI があっても、バックエンドは本文を検索していない。現行の Wiki 検索はタイトル検索のみである。 - -## 詳細表示 - -| API | 意味 | -| --- | --- | -| `GET /wiki/:id` | ID 指定 | -| `GET /wiki/title/:title` | タイトル完全一致 | - -`version` パラメータがある場合は指定改訂を表示する。なければ最新改訂を表示する。 - -返却には次が含まれる。 - -- `id` -- `title` -- `body` -- `revision_id` -- `pred` -- `succ` -- `updated_at` - -## リダイレクト - -改訂が `redirect` の場合、サーバはリダイレクト先タイトルへ `301 Moved Permanently` で遷移させる。 - -ただし現行 UI にはリダイレクト作成・編集導線がない。 - -## 存在確認 - -| API | 成功時 | 失敗時 | -| --- | --- | --- | -| `GET /wiki/:id/exists` | 204 | 404 | -| `GET /wiki/title/:title/exists` | 204 | 404 | - -## 差分表示 - -### API - -`GET /wiki/:id/diff?from=...&to=...` - -### 挙動 - -- `from` 省略時は空本文相当。 -- `to` 省略時は現行改訂。 -- `content` 改訂同士のみ比較可。 -- 結果は `context` / `added` / `removed` の列で返す。 - -## 作成 - -### API - -`POST /wiki` - -### 権限 - -member 以上。 - -### 入力 - -- `title` -- `body` -- `message` 任意 - -タイトルに対応する `TagName` を作成または再利用し、`Wiki::Commit.content!` で初回改訂を作る。 - -## 更新 - -### API - -`PUT /wiki/:id` - -### 権限 - -member 以上。 - -### 挙動 - -- `title` と `body` は必須。 -- タイトル変更は受け付けない。変更されていれば 422。 -- `message` は受け取れる。 -- `Wiki::Commit.content!` で改訂を作る。 - -### 競合制御の実態 - -`Wiki::Commit` は `base_revision_id` による競合検出を持つ。しかし `WikiPagesController#update` はクライアントから受けた基底版ではなく、更新直前にサーバ側で `page.current_revision.id` を読み直して使う。 - -つまり、衝突検出器はあるが、現行 API はクライアント起点の競合検出を十分に活かしていない。 - -## 履歴 - -### API - -`GET /wiki/changes` - -### パラメータ +nico カテゴリタグ一覧を取得する。 | パラメータ | 意味 | | --- | --- | -| `id` | Wiki ページ ID。未指定なら全体履歴 | +| limit | 件数。既定 20 | +| cursor | ISO8601 時刻。`updated_at < cursor` | -最大 200 件の `WikiRevision` を返す。`pred` は返るが、`succ` は常に NULL である。 +`linked_tags` を含む。 -# 素材集 +### PUT /tags/nico/:id -## 位置づけ +nico タグと内部タグの連携を更新する。member 以上必須。 -素材集は、タグに紐づく画像・動画・音声・参考 URL を管理する機能である。 +- 対象タグは nico カテゴリ必須。 +- 入力 `tags` を通常タグとして正規化する。 +- 連携先に nico カテゴリが含まれると 400。 +- `nico_tag_versions` に update が記録される。 -現行ではルートと画面は存在するが、トップナビでは `素材` メニューが `visible: false` になっている。つまり機能は実装され始めているが、正式導線としてはまだ隠し気味である。 +## ニジラー API -## 画面 +### GET /deerjikists/:platform/:code -| パス | 概要 | -| --- | --- | -| `/materials` | 素材一覧/タグ別素材ツリー | -| `/materials/new` | 素材追加 | -| `/materials/:id` | 素材詳細・編集 | -| `/materials/search` | import はあるがルートはコメントアウト | +外部識別子からニジラー対応タグを取得する。 -素材一覧は左サイドバーのタグ階層から対象タグを選び、対応する素材と子タグの素材を表示する。 +### PUT /deerjikists/:platform/:code -## API +ニジラー対応を作成・更新する。member 以上必須。 -| API | 権限 | 意味 | -| --- | --- | --- | -| `GET /materials` | 認証不要 | 素材一覧 | -| `GET /materials/:id` | 認証不要 | 素材詳細 | -| `POST /materials` | 認証済み、guest 可 | 素材作成 | -| `PUT /materials/:id` | member 以上 | 素材更新 | -| `DELETE /materials/:id` | member 以上 | 論理削除 | +入力: -## 一覧 +- tag_id -`GET /materials` は次を受け付ける。 +### DELETE /deerjikists/:platform/:code + +ニジラー対応を削除する。member 以上必須。 + +## 素材 API + +### GET /materials + +素材一覧を取得する。 | パラメータ | 意味 | | --- | --- | -| `page` / `limit` | ページング | -| `tag_id` | タグで絞り込み | -| `parent_id` | 親素材で絞り込み | +| page / limit | ページング | +| tag_id | 紐づくタグで絞る | +| parent_id | 親素材で絞る | -作成日時降順、ID 降順で返す。 +返却は `materials` と `count`。 -## 詳細 +### GET /materials/:id -`GET /materials/:id` は素材情報に加え、対応タグの Wiki 本文を `wiki_page_body` として返す。 +素材詳細を取得する。 -## 作成・更新 +返却には素材本体に加え、素材タグに対応する Wiki 本文 `wiki_page_body` が含まれる。 -### 作成 +### POST /materials -`POST /materials` は認証済みなら可能で、member 権限を要求しない。BTRC Hub では初回利用時に guest ユーザが自動発行されるため、公開時も guest に素材作成を許可する仕様とする。 +素材を作成する。現行実装ではログイン必須。 -入力は次の通り。 +計画仕様としては **member 以上に制限する**。当初は guest 作成可能の意図があったが、ファイルアップロードを伴うため、オブジェクトストレージへの大量投入・違法ファイル混入・容量爆撃のリスクを避ける。 -- `tag` -- `file` -- `url` +入力: -`tag` は必須。`file` または `url` のどちらかは必須。対象タグがなければ作成し、カテゴリは `material` になる。 +| パラメータ | 必須 | 意味 | +| --- | --- | --- | +| tag | 必須 | 対応タグ名 | +| file | file または url の一方必須 | 素材ファイル | +| url | file または url の一方必須 | 参考 URL | -### 更新 +タグが存在しない場合は `material` カテゴリで作成される。 -`PUT /materials/:id` は member 以上。 +### PUT /materials/:id -`tag` と、`file` または `url` が必須。`file` が無い状態で更新すると既存添付ファイルは purge される。 +素材を更新する。member 以上必須。 -## 現行の弱点 +- tag +- file +- url -- `material_versions` は未接続。 -- 素材追加は認証済み guest でも可能であり、公開時には再検討が必要。 -- ファイル種別の厳密なサーバ側制限は薄い。 -- タグ一意のため、1 タグに複数素材を持つ設計ではない。 +file が渡されない場合は既存ファイルを purge する。 -# プレビュー取得 +### DELETE /materials/:id -## API +素材を論理削除する。member 以上必須。 -| API | 意味 | +## Wiki API + +### GET /wiki + +Wiki 一覧・タイトル検索。 + +| パラメータ | 意味 | | --- | --- | -| `GET /preview/title` | 対象 URL の `` を取得 | -| `GET /preview/thumbnail` | Node screenshot でサムネイル生成 | +| title | タイトル部分一致。空なら全件 | -認証済ユーザのみ使用可能。 +本文検索は現行 `index` では行わない。 -## 注意点 +### GET /wiki/search -- URL が `http://` または `https://` で始まらなければ `http://` を補う。 -- `title` は `URI.open` と Nokogiri で取得する。 -- `thumbnail` は `node lib/screenshot.js` を shell 経由で実行し、MiniMagick で 180x180 に縮小する。 -- SSRF 対策・ドメイン制限・サイト別最適化は未整備。 +現行では `index` と同じ処理。 -公開前に最も危ない箇所の一つである。便利機能ではあるが、外部 URL 取得は防御を固めないと穴になる。 +### GET /wiki/:id / GET /wiki/title/:title -# 上映会(Theatre) +Wiki 詳細を取得する。 -## 会場表示 +| パラメータ | 意味 | +| --- | --- | +| version | revision id を指定して過去版取得 | -### API +返却: -`GET /theatres/:id` +- id +- title +- body +- revision_id +- pred +- succ +- updated_at -返却は `id`、`name`、`opens_at`、`closes_at`、作成者、作成・更新日時。現在上映中投稿や在席者は返さない。 +### GET /wiki/:id/exists / GET /wiki/title/:title/exists -## 在席通知と状態取得 +存在すれば 204、無ければ 404。 -### API +### GET /wiki/:id/diff -`PUT /theatres/:id/watching` +Wiki 差分を取得する。 -### 権限 +| パラメータ | 意味 | +| --- | --- | +| from | 古い revision id。省略時は空本文相当 | +| to | 新しい revision id。省略時は現行 revision | -認証済ユーザ。 +content revision 同士のみ比較可能。 -### 挙動 +### POST /wiki -- `theatre_watching_users.expires_at` を現在 + 30 秒へ更新する。 -- ホストが空、または現在ホストが在席していなければ、呼び出しユーザをホストにする。 -- `host_flg`、`post_id`、`post_started_at`、`watching_users` を返す。 +Wiki を作成する。member 以上必須。 -これは heartbeat と state fetch を兼ねた API である。 +入力: -## 次の投稿へ進める +- title +- body +- message 任意 -### API +処理: -`PATCH /theatres/:id/next_post` +1. title に対応する `tag_name` を取得または作成。 +2. `Wiki::Commit.create_content!` で page と revision を生成。 +3. `wiki_versions` に create 記録。 -### 権限 +### PUT /wiki/:id -認証済み、かつ現在ホストであること。 +Wiki を更新する。member 以上必須。 -### 挙動 +入力: -- `url LIKE '%nicovideo.jp%'` または `url LIKE '%youtube.com%'` の投稿から `RAND()` で 1 件選ぶ。 +- title +- body +- message 任意 +- base_revision_id 任意 + +現行では **タイトル変更も受け付ける**。タグがその tag_name を参照している場合、タグ版も更新記録される。 + +`base_revision_id` が現行最大 revision id と一致しない場合、409 conflict。 + +### GET /wiki/changes + +Wiki 改訂履歴を最大 200 件返す。 + +| パラメータ | 意味 | +| --- | --- | +| id | Wiki ページ ID 絞り込み | + +## プレビュー API + +### GET /preview/title + +指定 URL の HTML を取得し、`<title>` を読む。 + +### GET /preview/thumbnail + +Node スクリプトで対象 URL のスクリーンショットを取得し、縮小サムネイルを返す。 + +注意: + +- SSRF 対策、許可ドメイン、タイムアウト、サイズ上限などは仕様として明文化不足。 +- 外部ページ表示・プレビュー取得は公表前または利用拡大前に安全化必須。 + +## 上映会 API + +### GET /theatres/:id + +会場情報を取得する。 + +返却は `TheatreRepr.base`。現行では会場作成・一覧 API は無い。 + +### PUT /theatres/:id/watching + +ログイン必須。heartbeat と会場状態取得を兼ねる。 + +処理: + +1. `theatre_watching_users` の `expires_at` を現在 + 30 秒へ更新。 +2. host が空、または host が在席していなければ現在ユーザを host にする。 +3. `host_flg`, `post_id`, `post_started_at`, `watching_users` を返す。 + +### PATCH /theatres/:id/next_post + +ログイン必須かつ現在 host のみ。 + +- `nicovideo.jp` または `youtube.com` を含む投稿から RAND() で 1 件選ぶ。 - `current_post_id` と `current_post_started_at` を更新する。 -## コメント +### GET /theatres/:theatre_id/comments -| API | 意味 | +コメントを取得する。 + +| パラメータ | 意味 | | --- | --- | -| `GET /theatres/:theatre_id/comments` | コメント取得 | -| `POST /theatres/:theatre_id/comments` | コメント投稿 | +| no_gt | 指定番号より後のコメント | -`GET` は `no_gt` で差分取得できるが、現行コード上は `no DESC` で返す。旧仕様にあった `no ASC` ではない。 +注意: 現行実装は `order(no: :desc)` で返す。過去仕様書の「no ASC」は現行と異なる。 -`POST` は認証済ユーザなら可能。空本文は 422。採番は `theatre.next_comment_no` を `with_lock` で進める。 +### POST /theatres/:theatre_id/comments -## フロント同期 +コメント投稿。ログイン必須。 -Theatre はサーバ単独制御ではなく、ホストブラウザ依存の同期方式である。 +- 空本文は 422。 +- `theatre.next_comment_no` を `with_lock` で採番する。 -- 一定間隔で heartbeat とコメント差分取得を行う。 -- ホスト側ブラウザが進行判断する。 -- 次の投稿へ進めるのもホスト側ブラウザが `PATCH /next_post` を叩く。 +# フロントエンド画面仕様 -ホストが落ちると進行が止まる可能性がある。将来的にサーバ制御へ寄せる余地がある。 +## ルーティング -# 画面構成 +現行フロントの主要ルートは次。 -現行フロントの主要ルートは次の通り。 - -| パス | 概要 | +| パス | 画面 | | --- | --- | -| `/` | `/posts` へリダイレクト | -| `/posts` | 投稿一覧 | -| `/posts/search` | 投稿詳細検索 | -| `/posts/new` | 新規投稿 | -| `/posts/:id` | 投稿詳細 | -| `/posts/changes` | 耕作履歴。内部的に `/posts/versions` を使用 | -| `/tags` | タグ一覧・検索 | -| `/tags/nico` | ニコニコ連携 | -| `/theatres/:id` | 上映会場 | -| `/materials` | 素材集 | -| `/materials/new` | 素材追加 | -| `/materials/:id` | 素材詳細・編集 | -| `/wiki` | Wiki 検索 | -| `/wiki/:title` | Wiki 詳細 | -| `/wiki/new` | Wiki 新規作成 | -| `/wiki/:id/edit` | Wiki 編集 | -| `/wiki/:id/diff` | Wiki 差分 | -| `/wiki/changes` | Wiki 履歴 | -| `/users/settings` | ユーザ設定 | -| `/settings` | `/users/settings` へリダイレクト | -| `/tos` | 利用規約 MDX ページ | -| `/more` | その他メニュー | +| / | /posts へリダイレクト | +| /posts | 投稿一覧 | +| /posts/new | 投稿作成 | +| /posts/search | 投稿詳細検索 | +| /posts/:id | 投稿詳細 | +| /posts/changes | 投稿タグ変更履歴 | +| /tags | タグ一覧 | +| /tags/:id | タグ詳細 | +| /tags/:id/deerjikists | ニジラー紐づけ管理 | +| /tags/nico | ニコタグ連携 | +| /tags/changes | タグ履歴 | +| /theatres/:id | 上映会 | +| /materials | 素材一覧 | +| /materials/new | 素材追加 | +| /materials/:id | 素材詳細 | +| /wiki | Wiki 検索 | +| /wiki/:title | Wiki 詳細 | +| /wiki/new | Wiki 作成 | +| /wiki/:id/edit | Wiki 編集 | +| /wiki/:id/diff | Wiki 差分 | +| /wiki/changes | Wiki 履歴 | +| /users/settings | ユーザ設定 | +| /settings | /users/settings へリダイレクト | +| /tos | 利用規約 MDX | +| /more | その他ページ | -## ナビゲーション +## 投稿一覧画面 -トップナビはレスポンシブ対応済みである。 +- 投稿一覧を表示する。 +- `TagSidebar` が表示される。 +- サイドバーは、現在表示中の投稿に出てくるタグを最大 25 件まで拾い、カテゴリ順・名前順で出す。 +- モバイルではタグ一覧が折りたたみ表示になる。 +- 関連導線としてランダム投稿リンクがある。 -表示中の主メニューは次の通り。 +### Danbooru 風サイドバーの現行仕様 -- 広場 -- タグ -- 上映会 -- Wiki +サイドバーのタグ一覧は、全体 DB の人気順ではない。 -次はメニュー定義上はあるが `visible: false` で非表示。 +現行実装では、**現在表示中の投稿配列を先頭から舐め、出現したタグを最大 25 件までカテゴリ別に集め、各カテゴリ内を名前順に並べる**。 -- 素材 -- ユーザ -- 法規 -- 別名タグ -- 上位タグ -- 素材検索 -- 素材履歴 -- ユーザ一覧/詳細 +したがって、表示順は次に依存する。 -# バッチ・運用仕様 +1. 投稿一覧の現在ページ・並び順。 +2. 各投稿に含まれるタグ。 +3. カテゴリ順。 +4. カテゴリ内名前順。 + +## 投稿詳細画面 + +投稿詳細では埋め込み表示を行う。 + +| URL | 専用表示 | +| --- | --- | +| nicovideo.jp/watch/... | NicoViewer | +| youtube.com/watch?v=... | react-youtube | +| twitter.com / x.com の status URL | TwitterEmbed | +| その他 | 確認後 iframe | + +注意: + +- `youtu.be` は現行 `PostEmbed` の専用 YouTube 対応外。 +- YouTube Shorts も現行専用判定外の可能性が高い。 +- その他 URL は confirm 後に iframe 表示するため、X-Frame-Options 等で表示不能なサイトがある。 + +## タグ画面 + +- `/tags` は検索・一覧画面。 +- `/tags/:id` はタグ詳細。 +- `/tags/:id/deerjikists` は外部ニジラー対応編集画面。 +- `/tags/nico` は nico タグ連携画面。 +- `/tags/changes` はタグ版履歴。 + +## 素材画面 + +素材機能は現行フロントにルートがある。 + +- `/materials`: 素材一覧。 +- `/materials/new`: 素材追加。 +- `/materials/:id`: 素材詳細。 + +素材追加画面では、タグ、ファイル、参考 URL を入力する。ファイルは image / video / audio のプレビューに対応する。 + +`MaterialSearchPage` は存在するが、ルートはコメントアウトされており、現行導線からは未使用。 + +## Wiki 画面 + +- Wiki 検索。 +- Wiki 詳細。 +- Wiki 新規作成。 +- Wiki 編集。 +- Wiki 差分。 +- Wiki 履歴。 + +Markdown 表示には Wiki 自動リンク用 remark が組み込まれている。 + +## ユーザ設定画面 + +`/users/settings` では以下を扱う。 + +- 表示名更新。 +- 引継ぎコード表示。 +- 引継ぎコード更新。 +- 他ブラウザへの引継ぎ。 + +`settings` テーブルを使った汎用設定 UI は現行では未確認。 + +# 外部同期・バッチ + +## Nico 同期 + +Nico 同期は、ニコニコ動画の情報を投稿・タグへ反映する ETL 的機能である。 + +主な処理: + +1. 外部情報から動画 URL・タイトル・投稿日時・サムネイルを取得。 +2. 既存投稿を URL で探索し、無ければ作成。 +3. nico タグを `nico:` 接頭辞付きの nico カテゴリタグとして取り込む。 +4. `nico_tag_relations` に基づき内部タグへ展開する。 +5. `タグ希望`、`bot操作`、`ニコニコ`、`動画` 等のメタタグを付与する。 +6. ニジラー対応がなければ `ニジラー情報不詳` を補う。 + +## YouTube 同期 + +YouTube 同期は `Youtube::Sync` に実装されている。 + +主な処理: + +1. 検索語および playlist id から動画 ID を収集する。 +2. YouTube Data API の `videos` で詳細を取得する。 +3. 既存投稿は YouTube URL 正規表現で探索する。 +4. タイトル・公開日時・サムネイル URL を反映する。 +5. 新規投稿には `タグ希望`、`bot操作`、`YouTube`、`動画` を付与する。 +6. チャンネル ID と `deerjikists` の対応があれば、ニジラータグを付与する。 + +`YOUTUBE_API_KEY` が環境変数として必須。 ## 類似度計算 -| Rake task | 内容 | -| --- | --- | -| `post_similarity:calc` | 投稿同士の類似度再計算 | -| `tag_similarity:calc` | タグ同士の類似度再計算 | +Rake task により投稿・タグの類似度を再計算する。 -現行 schedule では `post_similarity:calc` の日次実行だけが定義されている。`tag_similarity:calc` は task はあるが、schedule には見当たらない。 +- 投稿類似度: タグ集合ベース。 +- タグ類似度: 投稿集合ベース。 +- 上位 20 件保存。 -## Wiki 移行 - -`wiki:migrate` は Gollum 形式の旧 Wiki を DB 版管理へ移行するタスクである。 - -現行コードと issue から見て、Wiki は Git/Gollum 管理から DB 管理へ移行済みである。 - -## ニコニコ同期 - -`nico:sync` は日次同期の中核タスクである。`config/schedule.rb` 上は 1 日 1 回、15:00 に production 環境で実行する定義になっている。 - -本番 crontab との時刻差が出る可能性はあるため、運用仕様として固定するなら `schedule.rb` と実際の crontab を合わせて管理する必要がある。 - -## R2 / Active Storage - -production では次を使用する。 - -- `config.active_storage.service = :r2` -- `storage.yml` の `r2` は S3 service -- 環境変数: - - `R2_ENDPOINT` - - `R2_ACCESS_KEY_ID` - - `R2_SECRET_ACCESS_KEY` - - `R2_BUCKET` -- `region: auto` -- `request_checksum_calculation: when_required` - -`routes.default_url_options[:host]` は `hub.nizika.monster/api` に設定されている。Active Storage URL 生成に関わるため、production の URL 設計では重要である。 - -# 非機能要件・運用上の注意 +# 非機能・運用仕様 ## 整合性 -現行で DB・モデル側にある主な整合性は次の通り。 +- URL は posts で一意。 +- post_tags の現行有効関係は生成列で一意。 +- materials の有効 URL は生成列で一意。 +- タグ名は一意。 +- Wiki ページは tag_name ごとに一意。 +- 投稿親子は複合主キーで重複不可。 +- 投稿親子の自己参照は禁止。 -- `posts.url` 一意。 -- HTTP/HTTPS URL のみ許可。 -- 有効な `post_tags(post_id, tag_id)` は一意。 -- `post_tags` は論理削除。 -- `tag_names.name` 一意。 -- `tags.tag_name_id` 一意。 -- `tag_implications(tag_id, parent_tag_id)` 一意。 -- `tag_implications` は自己参照禁止。 -- `post_versions(post_id, version_no)` 一意。 -- `post_versions` は読み取り専用。 -- `user_post_views` は複合主キー。 -- `theatre_comments` は `(theatre_id, no)` 複合主キー。 +## 監査性 -## セキュリティ上の弱点 +現行で保存される履歴: -公開前に明確に潰すべき弱点は次である。 - -1. **BAN 未強制** - - `users.banned` と `ip_addresses.banned` はあるが、認証・認可で強制されていない。 - -2. **preview API の SSRF リスク** - - 任意 URL をサーバから取得しうる。 - - 内部 IP・localhost・メタデータ IP などの拒否が必要。 - -3. **引継ぎコード認証の軽さ** - - コード漏洩時の乗っ取りリスクがある。 - - 権限昇格・重要操作では追加防御が必要。 - -4. **素材作成は guest 許可方針** - - `POST /materials` は認証済みなら可能で member 以上を要求しない。 - - 2026-04-20 の開発者ヒアリングで、公開時も guest に素材作成を許す方針とした。 - - ただし公開運用では、BAN 強制・アップロード制限・通報導線との組み合わせが必須である。 - -5. **投稿編集の競合制御なし** - - 同時編集時に後勝ちで上書きされる。 - -6. **iframe 埋め込み** - - 対応外サイトを iframe で表示できるが、外部ページの安全性・X-Frame-Options・ユーザ警告の設計が必要。 - -## 性能・ページング - -現行の主要既定件数は次の通り。 - -| 対象 | 既定/上限 | +| 対象 | 履歴 | | --- | --- | -| 投稿一覧 | `limit` 既定 20 | -| タグ一覧 | `limit` 既定 20 | -| タグ補完 | 最大 20 | -| ニコタグ一覧 | `limit` 既定 20、cursor あり | -| Wiki 検索 | 最大 20 | -| Wiki 履歴 | 最大 200 | -| 投稿関連 | 最大 20 | +| 投稿本体 | post_versions | +| 投稿タグ付与 | post_tags の created/deleted/discarded | +| タグ | tag_versions | +| nico タグ連携 | nico_tag_versions | +| Wiki | wiki_revisions / wiki_versions | +| 素材 | material_versions。ただし API 接続状況は要追加確認 | + +## セキュリティ上の注意 + +公表前または利用拡大前に仕様として潰すべき箇所。 + +1. **preview API の SSRF 対策** + - private IP 禁止、localhost 禁止、リダイレクト制御、サイズ上限、タイムアウト、Content-Type 制限が仕様化されていない。 +2. **iframe 外部表示** + - confirm はあるが、許可ドメインや CSP の明確仕様が無い。 +3. **引継ぎコード認証** + - 簡便だが漏洩時の被害が大きい。コード更新導線はあるが、セッション管理は薄い。 +4. **素材作成権限** + - 現行では guest でもログイン済みなら `POST /materials` できる。意図的か確認が必要。 +5. **IP BAN の remote_ip 前提** + - 本番プロキシ設定がズレると、全員同一 IP 扱いになる危険がある。 -# 計画仕様 +## パフォーマンス上の注意 -本章は現行コードに全面反映されていないが、今後の BTRC Hub にとって重要な仕様をまとめる。 +1. 投稿一覧はタグ・素材・Wiki を preload しているため、ページサイズやタグ数が増えると重くなる。 +2. タグサイドバーはクライアントで投稿配列からタグを収集する。1ページ件数増加時に効く。 +3. `order('RAND()')` は投稿数増加時に重くなる。 +4. Theatre の次動画選定も `RAND()` で、投稿数増加に弱い。 +5. `TagsController#index` の count は `q.size` であり、総件数計算として不安定。 +6. Wiki 本文検索は未実装であり、実装時には全文検索インデックス設計が必要。 -## 公開方針 +# 過去仕様書からの主要差分 -一般公開は段階的に進める。 +| 項目 | 2026-03-23 仕様書 | 2026-05-10 現行実装 | +| --- | --- | --- | +| BAN | 未接続と記述 | ApplicationController で IP / User BAN 強制済み | +| 投稿親子 | parent_id 前提の記述あり | post_implications 多対多 | +| 投稿履歴 | タグ履歴中心 | post_versions API あり | +| タグ更新 | name/category 中心 | aliases / parents / deerjikists 更新も実装 | +| Wiki 更新 | title 変更不可と記述 | title 変更可。tag version も記録 | +| Wiki 競合制御 | 活用されていないと記述 | base_revision_id を受け取り 409 を返す | +| 素材 | 旧仕様書では薄い/無し | materials / material_versions / 画面あり | +| Theatre コメント順 | no ASC と記述 | 現行 API は no DESC | +| YouTube | 一部計画寄り | Youtube::Sync 実装あり | +| 利用規約 | 計画 | `/tos` MDX ルートあり。ただし内容精査は別途必要 | -| 段階 | 意味 | -| --- | --- | -| 第 3 段階 | 誰でも閲覧可。投稿・編集は申請制 | -| 第 4 段階 | 履歴・差し戻し・BAN 運用が成熟した後、編集開放範囲を拡張 | +# 現行で「ある」と言ってよいもの -初回一般公開では、投稿・Wiki・タグ編集の中心権限は申請制とする。ただし、2026-04-20 の開発者ヒアリングにより、素材作成については guest にも許可する方針とする。これは全編集開放ではなく、素材集に限定した例外である。BAN 強制・アップロード制限・通報導線が弱い状態で広範な編集を開けるのは危ない。 +- 引継ぎコード認証。 +- guest / member / admin ロール。 +- IP BAN / User BAN の API 強制。 +- 投稿 CRUD のうち作成・参照・更新。 +- 投稿タグ検索、AND / OR / NOT、別名 canonicalise。 +- 投稿親子多対多。 +- 投稿本体バージョン履歴。 +- 投稿タグ付与履歴。 +- タグ一覧・検索・補完。 +- タグ別名・親タグ更新 API。 +- タグ履歴。 +- Nico タグ連携。 +- YouTube 同期。 +- ニジラー対応管理。 +- Wiki 作成・参照・更新・差分・履歴。 +- Wiki 競合検出。 +- 素材一覧・作成・更新・削除。 +- 上映会表示・在席・次投稿・コメント。 +- ユーザ設定の一部。 +- 利用規約ページのルート。 -## 公開前の必須装備 +# 現行で「あるつもり」は危険なもの -公開前に最低限必要なものは次である。 +- 通常のアカウント登録/ログイン。 +- 管理画面。 +- user / IP BAN の UI。 +- 投稿削除 API。 +- 素材 version 表示 API。 +- Theatre 一覧・作成・編集 API。 +- Wiki 本文検索。 +- Wiki 画像添付の画面導線。 +- Wiki redirect 作成機能。 +- 投稿更新の optimistic locking。 +- settings テーブルを使った汎用ユーザ設定。 +- `youtu.be` / Shorts / Bluesky / Pixiv 専用埋め込み。 +- preview API の安全仕様。 +- ゲスト編集の公表後運用。 -- 利用規約 -- プライバシーポリシー -- BAN 強制適用 -- 管理画面 -- 履歴閲覧と差し戻し -- 別名タグ管理 UI -- Wiki 本文検索 -- preview API の安全化 -- 投稿編集の排他制御 -- 素材作成 guest 許可に伴う安全制限 +# 開発者ヒアリング反映結果 -お遊び機能は価値があるが、運用防御より優先してはいけない。ここを間違えると、公開時に火事場劇場になる。 +以下は、仕様書作成時に確認した開発者回答と、それを受けた仕様上の扱いである。 -## 御意見番 +## H-001: 素材作成権限 -計画仕様(確定)として、御意見番を問い合わせ・不具合報告導線として設ける。 +現行 `POST /materials` は `current_user` があれば許可され、`gte_member?` を要求していない。 -目標は次の情報を自動添付できること。 +開発者回答: 当初の意図どおり guest でも素材作成可能にしていたが、オブジェクトストレージへ大量投入されるリスクがあるため、**耕作員(member)以上で制限する方向** とする。 -- 直前の操作ログ -- 画面スクリーンショット -- ブラウザ・OS など環境情報 -- 対象ページ URL -- ユーザ ID +仕様反映: 計画仕様として、素材作成は member 以上へ引き上げる。現行コードとの差分として明記する。 -これは単なる問い合わせフォームではなく、再現性のある報告を受け取るための保守装置である。 +## H-002: 投稿作成・更新で parent_post_ids 必須 -## BAN と管理画面 +現行 `parse_parent_post_ids` は `params.key?(:parent_post_ids)` が無いと例外にする。 -計画仕様(確定)として、管理画面では少なくとも次を扱う。 +開発者回答: 親投稿が無い投稿でも、API クライアントは必ず `parent_post_ids: ''` を送る仕様で確定。未指定によって誤って初期化されるのを防ぐためである。「変更なし」とする挙動は冪等性に反するため避ける。 -- ユーザ BAN / 解除 -- IP BAN / 解除 -- 問題投稿・問題編集の確認 -- 投稿版履歴からの復元 -- Wiki 履歴からの巻き戻し -- タグ別名作成・解除 -- タグサニタイズ規則管理 -- 御意見番一覧確認 +仕様反映: `POST /posts` および `PUT /posts/:id` は、親投稿が無い場合でも `parent_post_ids` を必須入力とする。空文字は「親投稿なし」を意味する。未指定は不正リクエストである。 -## 履歴管理 +補足: これは `PUT` を部分更新ではなく完全置換寄りの API として扱う設計である。外部 API 化する場合も、この思想を明記しなければ事故る。 -投稿版管理は現行実装済みだが、公開サービスとしては未完成である。 +## H-003: 投稿更新の排他制御 -今後必要なものは次。 +`post_versions` はあるが、`posts.version_no` は現行 schema に無く、`PUT /posts/:id` も `base_version_no` を受け取らない。 -- `restore` イベント型の明確化 -- URL・サムネイルを含む復元方針。ただし `thumbnail_base` は編集入力欄の追加後に復元対象へ含める -- 投稿削除・復元の版記録 -- 素材履歴の実装 -- Wiki 巻き戻し UI -- 誰が何を戻したかの監査表示 +開発者回答: 投稿更新に `base_version_no` を入れる計画を仕様に含める。 -## Wiki 計画 +仕様反映: 計画仕様として、投稿更新には optimistic locking を導入する。詳細画面で取得した `version_no` を更新時に `base_version_no` として送信し、サーバ側の現行版と一致しない場合は競合として扱う。 -Wiki は主要タグの説明を蓄積する知識基盤である。 +## H-004: Wiki redirect の扱い -優先すべき追加項目は次。 +`wiki_revisions.kind = redirect` は残っているが、`Wiki::Commit#redirect!` は廃止例外になっている。 -- 本文検索 -- 競合検出のクライアント連動 -- 下書き保存 -- 遷移時警告 -- 画像アップロード -- ページ保護 -- Wiki サイドバー -- リダイレクト編集 UI +開発者回答: Wiki redirect は今後廃止し、タグ別名へ一本化する。過去に使われた歴史もない。 -`wiki_assets` はスキーマだけ先に入っているため、実装するならモデル・API・Markdown 埋め込み記法まで一気に決めるべきである。2026-04-20 時点の方針は専用記法であり、通常の外部 URL と混同しない。 +仕様反映: Wiki redirect は現行スキーマ上の残存要素として扱い、将来仕様では採用しない。別名・表記揺れ・旧名からの到達は `tag_names.canonical_id` によるタグ別名機能へ集約する。 -## タグ計画 +## H-005: Wiki title 変更 -優先度が高いものは次。 +現行 `PUT /wiki/:id` は title 変更を受け付け、対応 `tag_name` を rename する。 -- 別名タグ作成 UI/API -- 別名タグ解除 UI/API -- 別名と Wiki 参照先の連動 -- 上位タグ専用管理画面 -- 同定文字・検索同定・禁止文字ルール -- タグ詳細ページの責務整理 +開発者回答: Wiki 編集画面からのタイトル変更を正式仕様として許可する。ただし、紐づくタグが存在しない場合はタグ詳細画面からの変更を促したい。 -特に同定文字は、現在の MySQL collation が `utf8mb4_0900_ai_ci` であるため、平仮名/片仮名、濁点、半濁点などの同一視問題と関係する。ここは仕様として正面から扱う必要がある。 +仕様反映: Wiki title 変更は正式仕様である。ただし、タグ名と Wiki title は同一知識ノードを指すため、Wiki title rename は実質的に tag rename である。権限・履歴・衝突処理では「Wiki の表題変更」ではなく「タグ名変更」として扱う。 -## 素材計画 +## H-006: Theatre コメント順 -素材集は実装が始まっているが、公開前に次を決める必要がある。 +現行 `GET /theatres/:theatre_id/comments` は `no DESC` で返す。 -- 1 タグ 1 素材でよいか、複数素材を許可するか。 -- `character` カテゴリは、下位区分の `material` を包括する代表素材タグとして許可し続ける。 -- `POST /materials` は公開時も guest に開ける。 -- `material_versions` をどう記録するか。 -- 画像・動画・音声以外を許可するか。 -- 素材ダウンロードや一括取得を許可するか。 +開発者回答: DESC で確定。 -## 投稿計画 +仕様反映: Theatre コメント一覧は `no DESC` を正式仕様とする。差分取得・画面表示では、クライアント側が必要に応じて並び替える。 -優先度が高いものは次。 +## H-007: TagSidebar のタグ順 -- 投稿編集の排他制御 -- 別投稿からタグをインポート -- URL・サムネイル復元を含む履歴復元。ただし `thumbnail_base` は入力欄追加後に復元対象化 -- 埋め込み対応拡張 -- 親投稿の用途整理 -- `/posts/changes` の将来的廃止と `/posts/versions` への一本化 +現行サイドバーは「表示中投稿に出現したタグ最大 25 件」をカテゴリ別・名前順で表示する。 -## 埋め込み対応計画 +開発者回答: 理想は Danbooru を踏襲したい。 -issue と既存仕様から見る優先候補は次。 +仕様反映: 現行仕様はカテゴリ別・名前順表示である。計画仕様として、Danbooru 風に現在の検索結果内での出現頻度・関連度を重視した順序へ寄せる。 -1. ニコニコ -2. YouTube -3. Pixiv -4. ニジカ投稿局 -5. Twitter / X -6. Bluesky -7. Tiktok -8. bilibili -9. その他 iframe +## H-008: settings テーブル -現行で専用実装済みなのはニコニコ、YouTube の一部、Twitter/X である。 +`settings` はあるが、汎用設定 API と UI は未確認。 -## 設定機能 +開発者回答: テーマ、タグ色、ミュートタグ、埋め込み自動再生などを settings に集約する方針で概ねよい。ただし、見た目にしか関係しない設定は `localStorage` にすることも検討中。 -`settings` テーブルは `key` と JSON `value` の汎用形式になっている。将来的には次を扱う想定である。 +仕様反映: サーバ側で永続化すべき設定と、端末ごとの表示設定を分ける。ミュートタグ・非表示タグなどユーザ体験に本質的な影響を与えるものは `settings` 候補、純粋な見た目は `localStorage` 候補とする。 -- テーマ -- タグカテゴリごとの色設定 -- ミュートタグ -- 非表示タグ -- 閲覧済の自動化設定 -- 埋め込み自動再生可否 -- 投稿一覧のページング/無限ローディング設定 +## H-009: 素材と material / character カテゴリ -現行 UI では表示名と引継ぎコードが中心であり、汎用設定は未接続である。 +現行 `Material#tag_must_be_material_category` は `character` または `material` を許可する。 -## 通知機能 +開発者回答: `character` タグに直接素材を 1 件だけ紐づける仕様でよい。現時点では `TagImplication` によって `character` を親タグ、関連する `material` を子タグとする運用をしており、大きな問題は生じていない。キャラクタータグは実質素材タグの集合代表である。ただし、タグ分けするほどでもないが異なる素材については、**包摂素材** として複数持たせたい。 -通知機能は未実装である。想定される通知は次。 +仕様反映: 現行仕様として、`character` は素材集合の代表タグ、`material` は具体素材タグとして扱う。複数素材は原則 `TagImplication` による子 `material` タグ化で表現する。計画仕様として、タグ分けするほどではない差分素材を単一 Material 配下へ複数保持する「包摂素材」を追加検討する。 -- 新規投稿 -- Wiki 変更 -- 自分が関わったタグ・投稿の変更 -- 上映会で誰かが見ている通知 -- 御意見番への返信/対応 +## H-010: 利用規約ページ -通知は機能横断基盤になるため、場当たり的に作ると後で詰まる。まず通知対象、既読管理、表示場所を決めるべきである。 +`/tos` ルートはある。 -## Theatre 計画 +開発者回答: 現行 `TOSPage.mdx` は現時点で有効な利用規約である。ただし草案の域を脱していないのも事実であり、基本はこれを踏襲する。 -Theatre は一般公開時点では実験機能扱いが妥当である。 +仕様反映: `/tos` は現行の有効な利用規約ページとして扱う。ただし、公表に向けて文面精査が必要な草案ベースの利用規約であることも併記する。 -将来的に必要なものは次。 +## H-011: preview API 安全仕様 -- 会場一覧 -- 会場作成 -- 会場編集 -- 複数会場 -- マイリスト連動 -- サーバ主導の進行制御 -- チャット管理 -- 誰かが見ている通知 +現行 preview は便利だが、公開サービスとしては SSRF 対策の仕様化が必要である。 -## 法務・ポリシー +開発者回答: サムネ取得に関してはかなり危うい。対案を後ほど議論する。 -### 利用規約 +仕様反映: preview API の安全仕様は未確定とする。ただし、既にサービスは公開状態であるため、これは「公表前の理想論」ではなく、現行公開サービスのリスクとして扱う。 -`/tos` ページは存在するが、公開用文書として内容精査が必要である。 +## H-012: YouTube 同期対象 -最低限含めるべき項目は次。 +`Youtube::Sync` は検索語と playlist id から動画 ID を集める。 -- 禁止コンテンツ -- 著作権侵害禁止 -- 外部リンク先の扱い -- 埋め込みの扱い -- ユーザ生成コンテンツの責任 -- アカウント停止条件 -- データ削除方針 -- 免責 -- 未成年配慮 -- 準拠法 / 管轄 +開発者回答: 現行はコード固定であるが、いずれは DB 管理に移行したい。汎用基盤化が夢である。 -### プライバシーポリシー +仕様反映: 現行仕様では YouTube 同期対象はコード固定。計画仕様として、検索語・playlist ID を DB 管理へ移行し、外部同期を汎用基盤化する余地を残す。 -最低限明示すべき収集情報は次。 +## H-013: 管理画面の最小範囲 -- IP アドレス -- 引継ぎコード -- Cookie / localStorage -- 編集履歴 -- アクセスログ -- 素材アップロード情報 -- 外部埋め込み先との通信 -- 御意見番の送信内容 +管理 API / UI はまだ薄い。 -# 追加ヒアリング反映事項 +開発者回答: 管理画面は公表後に追従して実装する。現時点では、Rails Console を管理ツールとして使用している。なお、サービス自体は既に公開済みであり、ここでいう「一般公開」は「公表」の意味である。 -2026-04-20 の開発者ヒアリングで、次の点を仕様として反映した。 +仕様反映: 現行管理運用は Rails Console ベースとする。管理画面は未実装の計画機能であり、公表後に追従実装する。ただし、BAN / 差し戻し / 最近の更新確認などは、利用者増加時に早期に必要となる。 -1. **素材集の権限** - - `POST /materials` は公開時も guest に許可する。 - - ただし、これは全編集権限の開放ではなく、素材作成に限定した例外である。 - - BAN 強制・アップロード制限・通報導線がないまま公開すると荒らし耐性が低いため、安全制限は別途必須である。 +# 実装優先度メモ -2. **投稿復元の範囲** - - 現行の復元対象は `title`、`tags`、`original_created_from`、`original_created_before` とする。 - - `thumbnail_base` は将来的に復元対象へ含める方針である。 - - ただし、現行 UI では `thumbnail_base` の入力欄がないため、入力欄を追加するまでは復元しない。 - - `url` についても、編集 UI と責務が整理されるまでは慎重に扱う。 +現行仕様を踏まえると、公表前または利用拡大前に優先すべき順は次。 -3. **`/posts/changes` の扱い** - - 旧タグイベント API として現時点では残す。 - - 将来的には廃止し、主導線は `/posts/versions` へ寄せる可能性が高い。 - -4. **Wiki 画像の記法** - - `wiki_assets` を使う場合は専用記法を採用する。 - - 具体案は `![代替テキスト](asset:1)` のような形式である。 - - 実装時は Markdown レンダラで `asset:` スキームを解決し、ページ内の `wiki_assets.no` または asset ID から添付 URL を生成する。 - -5. **素材カテゴリ** - - `character` は下位区分としての `material` を包括する代表素材タグとして扱う。 - - そのため、素材タグとして `material` だけでなく `character` も許容する。 - -引き続き判断が必要な点は次である。 - -1. **素材とタグの対応数** - - 現行は `tag_id` 一意で 1 タグ 1 素材に近い。将来もこれでよいか。 - -2. **Theatre の制御主体** - - 今後サーバ主導へ移すか、現行のホストブラウザ主導を続けるか。 - -3. **公開時の素材・iframe・preview の安全制限** - - どこまで許可し、どこから警告・拒否するか。 - -9. **上位タグ管理の正式 UI** - - 投稿詳細 D&D を正式管理導線とするか、専用管理画面を別途作るか。 - -10. **同定文字仕様** - - DB collation で勝手に同一視される範囲と、アプリ側で明示的に同定/禁止する範囲をどう分けるか。 +1. **投稿更新の排他制御** + - Wiki は 409 を返せる。投稿も合わせるべき。 +2. **素材権限の確認・修正** + - guest 作成可は危険。 +3. **管理画面 MVP** + - 現行運用は Rails Console ベース。少人数運用では成立するが、利用者が増えると UI が無い状態は運用負荷になる。 +4. **preview API 安全化** + - SSRF は利用拡大後に燃える種類の穴。 +5. **Wiki 本文検索** + - タグ整理基盤として価値に直結。 +6. **TagSidebar の並び順改善** + - 現行は「たまたま表示された投稿の先着タグ」で、検索支援として弱い。 +7. **Theatre コメント順・会場管理** + - 周辺機能なので中核より後。 # 付録 A: 現行 API 一覧 ## 投稿 -- `GET /posts` -- `GET /posts/random` -- `GET /posts/changes` -- `GET /posts/versions` -- `GET /posts/:id` -- `POST /posts` -- `PUT /posts/:id` -- `POST /posts/:id/viewed` -- `DELETE /posts/:id/viewed` +- GET /posts +- GET /posts/random +- GET /posts/changes +- GET /posts/versions +- GET /posts/:id +- POST /posts +- PUT /posts/:id +- POST /posts/:id/viewed +- DELETE /posts/:id/viewed ## タグ -- `GET /tags` -- `GET /tags/autocomplete` -- `GET /tags/with-depth` -- `GET /tags/:id` -- `PUT /tags/:id` -- `GET /tags/:id/deerjikists` -- `GET /tags/name/:name` -- `GET /tags/name/:name/deerjikists` -- `GET /tags/name/:name/materials` -- `POST /tags/:parent_id/children/:child_id` -- `DELETE /tags/:parent_id/children/:child_id` +- GET /tags +- GET /tags/autocomplete +- GET /tags/with-depth +- GET /tags/versions +- GET /tags/:id +- PUT /tags/:id +- PATCH /tags/:id +- GET /tags/:id/deerjikists +- PUT /tags/:id/deerjikists +- GET /tags/name/:name +- GET /tags/name/:name/deerjikists +- GET /tags/name/:name/materials +- POST /tags/:parent_id/children/:child_id +- DELETE /tags/:parent_id/children/:child_id -## ニコタグ +## Nico タグ -- `GET /tags/nico` -- `PUT /tags/nico/:id` +- GET /tags/nico +- PUT /tags/nico/:id ## Wiki -- `GET /wiki` -- `GET /wiki/search` -- `GET /wiki/changes` -- `GET /wiki/:id` -- `PUT /wiki/:id` -- `GET /wiki/:id/exists` -- `GET /wiki/:id/diff` -- `GET /wiki/title/:title` -- `GET /wiki/title/:title/exists` -- `POST /wiki` - -## ユーザ - -- `POST /users` -- `POST /users/verify` -- `GET /users/me` -- `POST /users/code/renew` -- `PUT /users/:id` - -## ニジラー - -- `GET /deerjikists/:platform/:code` -- `PUT /deerjikists/:platform/:code` -- `DELETE /deerjikists/:platform/:code` - -## プレビュー - -- `GET /preview/title` -- `GET /preview/thumbnail` - -## 上映会 - -- `GET /theatres/:id` -- `PUT /theatres/:id/watching` -- `PATCH /theatres/:id/next_post` -- `GET /theatres/:theatre_id/comments` -- `POST /theatres/:theatre_id/comments` +- GET /wiki +- GET /wiki/search +- GET /wiki/changes +- GET /wiki/:id +- PUT /wiki/:id +- GET /wiki/:id/exists +- GET /wiki/:id/diff +- GET /wiki/title/:title +- GET /wiki/title/:title/exists +- POST /wiki ## 素材 -- `GET /materials` -- `GET /materials/:id` -- `POST /materials` -- `PUT /materials/:id` -- `DELETE /materials/:id` +- GET /materials +- GET /materials/:id +- POST /materials +- PUT /materials/:id +- DELETE /materials/:id -# 付録 B: 主要永続データ一覧 +## ユーザ -| テーブル | 役割 | 状態 | -| --- | --- | --- | -| `posts` | 投稿リンク本体 | 使用中 | -| `post_versions` | 投稿版スナップショット | 使用中 | -| `post_tags` | 投稿とタグの関係、タグ履歴 | 使用中 | -| `tags` | 実体タグ | 使用中 | -| `tag_names` | タグ名・別名 | 使用中 | -| `tag_implications` | 上位タグ | 使用中 | -| `tag_name_sanitisation_rules` | タグ名置換規則 | 内部使用 | -| `nico_tag_relations` | ニコタグと内部タグの連携 | 使用中 | -| `deerjikists` | 外部人物識別子とタグの対応 | 使用中 | -| `wiki_pages` | Wiki ページ | 使用中 | -| `wiki_revisions` | Wiki 改訂 | 使用中 | -| `wiki_lines` | Wiki 行本文 | 使用中 | -| `wiki_revision_lines` | 改訂と行の対応 | 使用中 | -| `wiki_assets` | Wiki 画像 | スキーマのみ | -| `materials` | 素材 | 使用中 | -| `material_versions` | 素材履歴 | スキーマのみ | -| `users` | ユーザ | 使用中 | -| `ip_addresses` | IP 記録 | 使用中、BAN 未強制 | -| `user_ips` | ユーザと IP の対応 | 使用中 | -| `user_post_views` | 閲覧済 | 使用中 | -| `settings` | 汎用設定 | 未接続 | -| `post_similarities` | 投稿類似度 | 使用中 | -| `tag_similarities` | タグ類似度 | 一部使用/表示薄い | -| `theatres` | 上映会場 | 使用中 | -| `theatre_comments` | 上映会コメント | 使用中 | -| `theatre_watching_users` | 上映会在席 | 使用中 | -| Active Storage 系 | 添付ファイル | 使用中 | +- POST /users +- POST /users/verify +- GET /users/me +- POST /users/code/renew +- PUT /users/:id -# 付録 C: 参照した主要ソース +## ニジラー -- `backend/config/routes.rb` -- `backend/db/schema.rb` -- `backend/app/controllers/*` -- `backend/app/models/*` -- `backend/app/services/post_version_recorder.rb` -- `backend/app/services/wiki/commit.rb` -- `backend/app/representations/*` -- `backend/lib/tasks/*.rake` -- `backend/config/storage.yml` -- `backend/config/schedule.rb` -- `backend/config/environments/production.rb` -- `frontend/src/App.tsx` -- `frontend/src/types.ts` -- `frontend/src/lib/*` -- `frontend/src/pages/*` -- `frontend/src/components/*` -- `btrc-hub.wiki.zip` 内の開発 Wiki -- `btrc-hub.issue.sql` -- `BTRC_Hub_仕様書_2026-03-23.md` \ No newline at end of file +- GET /deerjikists/:platform/:code +- PUT /deerjikists/:platform/:code +- DELETE /deerjikists/:platform/:code + +## プレビュー + +- GET /preview/title +- GET /preview/thumbnail + +## 上映会 + +- GET /theatres/:id +- PUT /theatres/:id/watching +- PATCH /theatres/:id/next_post +- GET /theatres/:theatre_id/comments +- POST /theatres/:theatre_id/comments + +# 付録 B: 主要テーブル一覧 + +| テーブル | 役割 | +| --- | --- | +| posts | 投稿リンク本体 | +| post_implications | 投稿親子関係 | +| post_tags | 投稿タグ関係と付与履歴 | +| post_versions | 投稿本体・タグ・親投稿構成の版 | +| tags | タグ実体 | +| tag_names | タグ名と別名 | +| tag_implications | タグ親子関係 | +| tag_versions | 通常タグ履歴 | +| nico_tag_versions | nico タグ連携履歴 | +| tag_name_sanitisation_rules | タグ名サニタイズ規則 | +| nico_tag_relations | nico タグと内部タグの対応 | +| deerjikists | 外部人物識別子とタグの対応 | +| materials | 素材 | +| material_versions | 素材履歴 | +| wiki_pages | Wiki ページ | +| wiki_revisions | Wiki 改訂 | +| wiki_lines | Wiki 行本文ストア | +| wiki_revision_lines | Wiki 改訂と行の対応 | +| wiki_versions | Wiki スナップショット履歴 | +| wiki_assets | Wiki 添付資産 | +| users | ユーザ | +| ip_addresses | IP 記録と IP BAN | +| user_ips | ユーザと IP の対応 | +| user_post_views | 閲覧済み | +| post_similarities | 投稿類似度 | +| tag_similarities | タグ類似度 | +| theatres | 上映会 | +| theatre_comments | 上映会コメント | +| theatre_watching_users | 上映会在席 | +| settings | 汎用設定候補 | + +# 付録 C: 確認した主要ソース + +- backend/config/routes.rb +- backend/db/schema.rb +- backend/app/controllers/application_controller.rb +- backend/app/controllers/posts_controller.rb +- backend/app/controllers/tags_controller.rb +- backend/app/controllers/nico_tags_controller.rb +- backend/app/controllers/post_versions_controller.rb +- backend/app/controllers/tag_versions_controller.rb +- backend/app/controllers/wiki_pages_controller.rb +- backend/app/controllers/materials_controller.rb +- backend/app/controllers/theatres_controller.rb +- backend/app/controllers/theatre_comments_controller.rb +- backend/app/controllers/users_controller.rb +- backend/app/models/post.rb +- backend/app/models/tag.rb +- backend/app/models/tag_name.rb +- backend/app/models/material.rb +- backend/app/models/wiki_page.rb +- backend/app/models/wiki_revision.rb +- backend/app/models/user.rb +- backend/app/models/ip_address.rb +- backend/app/services/wiki/commit.rb +- backend/app/services/youtube/sync.rb +- frontend/src/App.tsx +- frontend/src/components/PostEmbed.tsx +- frontend/src/components/TagSidebar.tsx +- frontend/src/pages/materials/* \ No newline at end of file