(過去制作物)世界一平和なSNS EnChat
enChat — 設計と技術的特徴
「世界一平和なSNS」 — 登録不要・完全匿名のリアルタイムチャット
1. プロジェクト概要
2019年ごろ、このようなサービスを作ってみました。
enChat は、ユーザー登録不要で誰でもすぐに参加できる匿名リアルタイムチャット SNS でした。
トピックベースのチャットルームを中心に、カテゴリ分類・Twitter トレンド連携・NG ワードフィルタなどの機能を備えています。
当時無料だったTwitterのAPIを利用して、トレンドを自動的に取得し、それをスレッドにしていました。
また、投稿前にPythonの自然言語パッケージで暴言などを抑制するプログラムも実装しています。
| 項目 | 内容 |
|---|---|
| URL | https://en-chat.net |
| コンセプト | 脱力系SNS — いいね機能なし・評価なし・プレッシャーのない空間 |
| 開発者 | 佐土原 裕貴 (Yuki Sadohara) |
プロジェクト凍結の理由
EnChatは匿名、LINEのようなリアルタイム投稿可能、ログイン不要との要件で開発したが、 現状、個人の範囲だと攻撃が来た時に対応する術がない。 また旧TwitterのAPIの無料配布が終わってしまったため、 肝であるTwitterトレンドから記事を自動生成することが個人の予算だと難しくなってしまった。 などの理由でこのサービスは停止しました。
2. 技術スタック
| レイヤー | 技術 |
|---|---|
| フロントエンド | Vue.js 2.5 (SPA) + Vue Router 3 (history mode) |
| データベース | Firebase Realtime Database (BaaS) |
| ビルドツール | Webpack 3 + Babel 6 (vue-cli 2.x テンプレート) |
| ホスティング | さくら VPS + nginx |
| 外部 API | Twitter API v1.1 (トレンド取得 + 自動投稿) |
| アナリティクス | Google Analytics (UA) |
主要ライブラリ
| パッケージ | 用途 |
|---|---|
firebase ^6.6.1 | Realtime Database との通信 |
vue-router ^3.0.1 | クライアントサイドルーティング |
vue-meta ^2.3.3 | ページごとの動的 OGP / メタタグ生成 |
vue-nl2br ^0.1.2 | メッセージ内の改行を <br> に変換 |
vue-smoothscroll ^0.2.0 | 新メッセージ時のスムーズスクロール |
vue-intersect ^1.1.6 | Intersection Observer によるコンテンツ遅延表示 |
vue-loading-template ^1.3.2 | ローディングスピナー |
js-cookie ^2.2.1 | Cookie によるユーザー名の永続化 |
body-scroll-lock ^2.6.4 | モーダル表示時の背面スクロール抑止 |
3. アーキテクチャ
- サーバーレス: 独自バックエンドを持たず、Firebase Realtime Database を直接操作する BaaS 構成
- Vuex 不使用: 状態管理ライブラリを使わず、各コンポーネントの
data()でローカル管理。Firebase がデータの Single Source of Truth - リアルタイム同期: Firebase の
.on('value')リスナーにより、メッセージやトピック一覧がリアルタイムに更新 - 完全匿名: ユーザー認証なし。ユーザー名は Cookie に保存するのみ(デフォルト名: 「えんちゃったー」)
4. Firebase データモデル
データベースパス
| パス | 構造 | 用途 |
|---|---|---|
/topic/{topicId} | { topicId, topicTitle, topicDesc, topicCategory, messageNum, topicCreated, topicUpdated } | トピックのメタデータ |
/topic/{topicId}/message/{pushKey} | { topicId, num, name, message, timestamp } | トピック内のメッセージ (チャット表示用) |
/message/{pushKey} | { topicId, name, message, timestamp } | グローバルメッセージフィード (注目トピック算出用) |
/report/{pushKey} | { topicId, messageId, messageNum, messageContent, timestamp } | ユーザーからの通報 |
カテゴリ ID マッピング
| ID | カテゴリ名 | URL スラッグ |
|---|---|---|
| 0 | ニュース | news |
| 1 | アニメ・漫画 | anime-comic |
| 2 | ゲーム | game |
| 3 | 芸能人・Youtuber | entertainer |
| 4 | 音楽 | music |
| 5 | 番組実況 | live |
| 6 | 旅行 | travel |
| 7 | 仕事 | business |
| 8 | 金融・株 | finance |
| 9 | ファッション・美容 | fashion |
| 10 | 健康 | health |
| 99 | Twitter トレンド | twitter-trend |
タイムスタンプ形式
Y/M/D/ h:m:s(例: 2020/2/7/ 14:30:45)
5. ルーティング
Vue Router (history mode) による 9 つのルート:
| パス | コンポーネント | 概要 |
|---|---|---|
/ | index.vue | ホーム — 注目トピック・カテゴリ一覧・Twitter トレンド |
/topic/:topicId | topic.vue | リアルタイムチャットルーム |
/category/:categoryName | category.vue | カテゴリ別トピック一覧 |
/search/:query | search.vue | トピック検索結果 |
/about | about.vue | enChat について |
/terms | terms.vue | 利用規約 |
/privacy | privacy.vue | プライバシーポリシー |
/aboutme | aboutme.vue | 開発者・お問い合わせ |
* | notfound.vue | 404 ページ |
6. 主要コンポーネントの設計
6.1 index.vue — ホームページ (1201行)
3つのタブで構成:
- 注目タブ:
/message/の直近 50 件からユニークな topicId を最大 10 件抽出し、各トピックに.on('value')リスナーを設定。トピックカードには直近 3 件のメッセージを表示 - カテゴリタブ: 各カテゴリの直近 11 トピックをインラインで表示。「もっと見てみる」で
/category/へ遷移 - トレンドタブ:
topicCategory === 99のトピック (Twitter トレンドから自動生成) を表示
注目トピックのアルゴリズム: グローバル /message/ フィードの直近 50 メッセージから、異なる topicId を出現順に最大 10 件抽出。直近にメッセージが多いトピックが上位に表示される。
6.2 topic.vue — チャットルーム (1337行)
リアルタイムチャットのコア機能:
- リアルタイム受信:
.on('value')でメッセージをリアルタイム同期 - メッセージ送信: NG ワードチェック → トリム・空文字チェック → 300文字制限 →
/topic/{id}/message/と/message/へ二重書き込み - 自動スクロール: ユーザーが最下部にいる場合のみ、新メッセージ到着時にスムーズスクロール
- トピック上限: 1000 メッセージに達すると自動ロック。"FIN" マーカーを挿入し入力を無効化
- 通報機能: メッセージごとに「報告」ボタン → モーダルで報告カテゴリ選択 →
/report/に書き込み - トピック未存在: Firebase から null が返った場合は「トピックが見つかりません」を表示
6.3 header.vue — グローバルヘッダー (1060行)
全ページ共通の固定ヘッダー:
- トピック作成: カテゴリ選択 + タイトル (50文字) + 説明 (300文字) → Firebase にトピック作成。「※トピックの有効期限は3日です」と表示
- 検索: 入力値を
/search/{query}にルーティング。日本語 IME の確定イベント対応あり - 設定: Cookie に保存するユーザー名の変更
- ナビゲーション: 各静的ページへのリンク
6.4 category.vue — カテゴリページ (737行)
- URL スラッグからカテゴリ ID を switch 文でマッピング
orderByChild('topicCategory').equalTo(id).limitToLast(20)で取得- カテゴリが空の場合は「まだトピックがありません」メッセージ表示
6.5 search.vue — 検索結果 (191行)
- Firebase の
startAt(query).endAt(query + '\uf8ff')による前方一致検索のみ - Firebase Realtime Database に全文検索機能がないための制約
7. コンテンツモデレーション
NG ワードフィルタ (checkNg.js)
3 カテゴリの正規表現によるフィルタリング:
| カテゴリ | 内容 | 件数 |
|---|---|---|
adultNgReg | 性的コンテンツ (日本語カタカナ・漢字) | 約 130 語 |
castNg | 差別用語・ヘイトスピーチ | 約 20 語 |
customNg | カスタム禁止語 (「殺す」など) | 可変 |
- クライアントサイド:
topic.vueでメッセージ送信前にチェック。NG 検出時は「NGワードが含まれています」モーダルを 2.5 秒表示 - バッチスクリプト:
getTrends.jsで Twitter トレンドのトピック自動生成前にチェック - 制限: クライアントサイドのみの実装のため、Firebase への直接リクエストでバイパス可能
8. バッチ処理
getTrends.js — Twitter トレンド連携
30 分ごとに cron で実行:
- Twitter API (
trends/place.json, WOEID: 23424856 = 日本) でトレンドを取得 - NG ワードフィルタでスクリーニング
「{トレンド名}」を語るトピックというタイトルで Firebase にトピック自動生成 (topicCategory: 99)- enChat の Twitter アカウントからプロモーションツイートを自動投稿
delete.js — トピック自動削除
定期実行:
- 3 日前に作成されたトピックを
topicCreatedで検索 - 該当トピックを物理削除 (
remove()) - グローバル
/message/ノードを全件削除
9. UI / UX 設計
ブランドカラー
| 用途 | カラー |
|---|---|
| プライマリ (ヘッダー, アクティブタブ) | #ea4827 (レッドオレンジ) |
| トピックタイトル | #39a9bf (ティール) |
| ボタングラデーション | linear-gradient(45deg, #ea4827, #ff9393) |
| アクティブリンク / 送信ボタン | #4169e1 (ブルー) |
| テキスト | #2c3e50 (ダークブルーグレー) |
フォント
- ロゴ: Rubik (Google Fonts)
- 本文: Lato, Noto Sans JP, Hiragino Kaku Gothic ProN, Meiryo
レスポンシブデザイン
単一ブレークポイント: 479px
| PC (≥479px) | モバイル (<479px) | |
|---|---|---|
| レイアウト | メインコンテンツ + 25vw サイドバー | シングルカラム (全幅) |
| サイドバー | カテゴリ一覧 + Twitter トレンド表示 | 非表示 |
| ヘッダー | 固定幅 (max 980px) | 固定全幅 |
| ホームのタブ | 非表示 (サイドバーで代替) | 3タブ切り替え (注目/カテゴリ/トレンド) |
| トピック作成モーダル | 50vw × 50vh (中央配置) | 100vw × 100vh (全画面) |
| チャット入力 | 下部固定 (padding: 0 15vw) | 下部固定 (全幅) |
10. プロジェクト構成
EnChat/
├── index.html # SPA エントリ HTML (GA, OGP, Google Fonts)
├── package.json # 依存管理・npm スクリプト
├── config/
│ └── index.js # dev/build 設定 (ホスト, ポート, ESLint, ソースマップ)
├── build/ # Webpack ビルドスクリプト群
│ ├── webpack.base.conf.js # ベース設定 (エントリ, ローダー, エイリアス)
│ ├── webpack.dev.conf.js # 開発サーバー設定 (HMR, historyApiFallback)
│ └── webpack.prod.conf.js # 本番ビルド (UglifyJS, CSS抽出, チャンク分割)
├── src/
│ ├── main.js # Vue/Firebase 初期化
│ ├── App.vue # ルートコンポーネント (header + router-view + footer)
│ ├── checkNg.js # NG ワードフィルタ
│ ├── getTrends.js # Twitter トレンド取得バッチ
│ ├── delete.js # トピック削除バッチ
│ ├── router/
│ │ └── index.js # ルート定義 (9 ルート)
│ ├── components/
│ │ ├── index.vue # ホームページ (1201行)
│ │ ├── topic.vue # チャットルーム (1337行)
│ │ ├── header.vue # グローバルヘッダー (1060行)
│ │ ├── footer.vue # グローバルフッター (73行)
│ │ ├── category.vue # カテゴリ別一覧 (737行)
│ │ ├── search.vue # 検索結果 (191行)
│ │ ├── about.vue # enChat について (95行)
│ │ ├── terms.vue # 利用規約 (119行)
│ │ ├── privacy.vue # プライバシーポリシー (117行)
│ │ ├── aboutme.vue # お問い合わせ (84行)
│ │ └── notfound.vue # 404 ページ (85行)
│ └── assets/
│ └── svg/ # 22 個の SVG アイコン
└── test/
└── unit/ # Jest ユニットテスト
11. npm スクリプト
| コマンド | 動作 |
|---|---|
npm run dev | Webpack Dev Server 起動 (127.0.0.1:8080, HMR 有効) |
npm run build | 本番ビルド → dist/ に出力 |
npm run lint | ESLint 実行 (.js, .vue) |
npm run test | Jest ユニットテスト |
12. 設計上の特筆点
意図的な設計判断
| 判断 | 理由 |
|---|---|
| ユーザー登録なし | セキュリティインシデントの教訓。個人情報を一切保持しない |
| 「いいね」機能なし | 社会的プレッシャーのない空間を設計するため |
| 3日でトピック削除 | Firebase 無料枠のストレージ節約 + コンテンツの鮮度維持 |
| Go バックエンドの廃止 | 当初 Go で実装 → Firebase のみの構成に移行し保守コストを削減 |
