トップへ戻る

(過去制作物)世界一平和なSNS EnChat

enChat — 設計と技術的特徴

「世界一平和なSNS」 — 登録不要・完全匿名のリアルタイムチャット

1. プロジェクト概要

2019年ごろ、このようなサービスを作ってみました。 enChat は、ユーザー登録不要で誰でもすぐに参加できる匿名リアルタイムチャット SNS でした。
トピックベースのチャットルームを中心に、カテゴリ分類・Twitter トレンド連携・NG ワードフィルタなどの機能を備えています。 当時無料だったTwitterのAPIを利用して、トレンドを自動的に取得し、それをスレッドにしていました。 また、投稿前にPythonの自然言語パッケージ暴言などを抑制するプログラムも実装しています。

項目内容
URLhttps://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
外部 APITwitter API v1.1 (トレンド取得 + 自動投稿)
アナリティクスGoogle Analytics (UA)

主要ライブラリ

パッケージ用途
firebase ^6.6.1Realtime 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.6Intersection Observer によるコンテンツ遅延表示
vue-loading-template ^1.3.2ローディングスピナー
js-cookie ^2.2.1Cookie によるユーザー名の永続化
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芸能人・Youtuberentertainer
4音楽music
5番組実況live
6旅行travel
7仕事business
8金融・株finance
9ファッション・美容fashion
10健康health
99Twitter トレンドtwitter-trend

タイムスタンプ形式

Y/M/D/ h:m:s(例: 2020/2/7/ 14:30:45


5. ルーティング

Vue Router (history mode) による 9 つのルート:

パスコンポーネント概要
/index.vueホーム — 注目トピック・カテゴリ一覧・Twitter トレンド
/topic/:topicIdtopic.vueリアルタイムチャットルーム
/category/:categoryNamecategory.vueカテゴリ別トピック一覧
/search/:querysearch.vueトピック検索結果
/aboutabout.vueenChat について
/termsterms.vue利用規約
/privacyprivacy.vueプライバシーポリシー
/aboutmeaboutme.vue開発者・お問い合わせ
*notfound.vue404 ページ

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 で実行:

  1. Twitter API (trends/place.json, WOEID: 23424856 = 日本) でトレンドを取得
  2. NG ワードフィルタでスクリーニング
  3. 「{トレンド名}」を語るトピック というタイトルで Firebase にトピック自動生成 (topicCategory: 99)
  4. enChat の Twitter アカウントからプロモーションツイートを自動投稿

delete.js — トピック自動削除

定期実行:

  1. 3 日前に作成されたトピックを topicCreated で検索
  2. 該当トピックを物理削除 (remove())
  3. グローバル /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 devWebpack Dev Server 起動 (127.0.0.1:8080, HMR 有効)
npm run build本番ビルド → dist/ に出力
npm run lintESLint 実行 (.js, .vue)
npm run testJest ユニットテスト

12. 設計上の特筆点

意図的な設計判断

判断理由
ユーザー登録なしセキュリティインシデントの教訓。個人情報を一切保持しない
「いいね」機能なし社会的プレッシャーのない空間を設計するため
3日でトピック削除Firebase 無料枠のストレージ節約 + コンテンツの鮮度維持
Go バックエンドの廃止当初 Go で実装 → Firebase のみの構成に移行し保守コストを削減

image