PPPOST は、Twitter/X・Bluesky・Mastodon に一つの画面から同時投稿できるマルチプラットフォーム対応のソーシャルメディア投稿アプリケーションです。Svelte 製フロントエンドと Netlify Functions ベースのバックエンドを備えたモノレポ構成になっています。
- Twitter(X)・Bluesky・Mastodon (mastodon.cloud) への同時投稿
※他サーバーへの対応希望は issue で相談してください
- 各 SNS への接続設定を完了する
- 投稿内容を入力して「Post」を実行する
text: 起動時に投稿本文へ即反映される。改行や記号はクエリ文字列としてエンコードして指定する。url:textが未指定の場合に投稿本文へ反映される。- URL のみが指定されている場合は対象ページのタイトルを取得し、
{タイトル} - {URL}の形式で本文へ整形する。 - Swarm 対応:どちらかでセットされた本文内に
https://swarmapp.com/.../checkin/...形式の URL が含まれると、自動でスクレイピング API を呼び出し、取得したチェックイン内容で本文を置き換える。
/frontend: TypeScript・Vite・PWA サポートを備えた Svelte 4 の SPA/backend: Netlify Functions で構築したサーバーレス API 群- ルートディレクトリ : 共有設定やドキュメントを配置
主要技術スタック
- フロントエンド: Svelte 4、TypeScript、Vite 5、Bootstrap 5、@atproto/api (Bluesky 用)
- バックエンド: Node.js サーバーレス関数、twitter-api-v2、cheerio/jsdom
- 認証: Twitter OAuth、Mastodon トークン、Bluesky SDK
cd frontend
npm install
npm run dev # 開発サーバーを http://localhost:8080 で起動
npm run build # プロダクションビルド
npm run check # TypeScript/Svelte の型チェックcd backend
npm install
npm run dev # Netlify Functions を http://localhost:9000 で起動- ターミナル 1:
cd frontend && npm run dev - ターミナル 2:
cd backend && npm run dev
本プロジェクトでは、新機能や重要な修正に OpenSpec を活用した仕様駆動開発を採用しています。
- プロポーザル作成:
/openspec/changes/{change-id}/proposal.mdで変更の背景・内容・影響を記述 - 仕様定義:
/openspec/changes/{change-id}/specs/{capability}/spec.mdで要件とシナリオを定義 - タスク分割:
/openspec/changes/{change-id}/tasks.mdで実装タスクをステップ分割 - 実装: タスクに沿って実装
- 検証:
openspec validateでプロポーザルと仕様の整合性を確認
- PROJECT_KEY:
PPP(PPPOST の略) - コミットメッセージ: 日本語、チケット番号を接頭辞に付与(例:
PPP-003 リプライ選択グループ化を修正) - Git ワークフロー: 過去のコミットを書き換える操作は禁止し、常に通常の
git commitを使用
本プロジェクトでは、proposal と spec の両方にチケット番号を付与し、課題管理システムで親子関係を管理します。
Change ID (Proposal ID):
- 形式:
PPP-{TASK_ID}-{descriptive-name} - 例:
PPP-005-add-threads-support
Spec Directory Name:
- 形式:
PPP-{TASK_ID}-{descriptive-name} - 例:
PPP-006-threads-ui,PPP-008-threads-api - proposal とは異なるチケット番号を使用
構造例:
openspec/changes/PPP-005-add-threads-support/ ← proposal (親)
├── proposal.md
├── tasks.md
└── specs/
├── PPP-006-threads-ui/ ← spec1 (子)
│ └── spec.md
├── PPP-008-threads-api/ ← spec2 (子)
│ └── spec.md
└── PPP-010-threads-notification/ ← spec3 (子)
└── spec.md
Proposal ファイル:
- セクション名は英語のみ:
## Why,## What Changes,## Impact(OpenSpec パーサー要件) - タイトルは日本語:
# リプライ選択グループ化の修正 - 本文は日本語: 背景説明、変更内容、影響範囲などは日本語で記述
Spec ファイル:
- 見出しは英語と日本語の併記:
#### Scenario: Same content posted to multiple SNS(同じ内容を複数 SNS に投稿)- 英語見出しで OpenSpec ツールとの互換性を維持
- 括弧内の日本語で日本語話者の理解を容易にする
- 本文は日本語: WHEN/THEN 条件などの仕様記述は日本語で記述
- Requirement には SHALL/MUST を含める: OpenSpec 検証に必須
詳細は /openspec/AGENTS.md を参照してください。
- 新しいソーシャルプラットフォーム追加
現時点では共通化された手順が確立されていないため、既存の Mastodon/Bluesky/Twitter 実装を参考に個別対応してください。 - サーバーレス関数の基本形
exports.handler = async (event, context) => {
// CORS ヘッダーを付与
// POST の場合は event.body をパース
// { statusCode, headers, body } を返す
}- フロントエンド状態管理
- 認証トークンはローカルストレージに保存しており、現状は平文 JSON で保持される
- 状態はコンポーネント内のリアクティブ変数で管理
- プラットフォーム固有の設定は別モジュールへ分離
- Twitter/X: Yahoo リアルタイム検索経由でスクレイピングするため、URL が省略形(
docs.github.com/ja/copilot/get…)で含まれる場合がある。投稿時刻の正確な取得は不可能。 - Bluesky: Bluesky SDK 経由でプレーンテキストとして取得
- Mastodon: Mastodon API 経由で取得。HTML エンティティ(
,<,"など)が含まれる場合がある
同一内容の投稿を複数 SNS から取得した際、リプライ元選択ドロップダウンで1つのグループとして表示するため、以下の正規化処理を行います(frontend/src/lib/MainContent.ts:normalizeTextForGrouping):
- URL の除去:
https?://[^\s]+パターンにマッチする URL を削除 - HTML タグの除去:
<[^>]+>パターンにマッチするタグを削除 - HTML エンティティのデコード:
,<,>,",',&を対応する文字に変換 - 空白の正規化: 連続する空白文字(スペース、タブ、改行)を1つのスペースに統一
- 前後の空白削除:
trim()で除去 - 50%比較: 正規化後のテキストの最初の50%(最小10文字、最大100文字)をグループ化キーとして使用
この正規化により、以下のような微妙な違いがあっても同一グループとして扱われます:
// Twitter/X (Yahoo経由)
"docs.github.com/ja/copilot/get… 会社で Copilot Business...。 3〜4日..."
// Bluesky
"会社で Copilot Business...。3〜4日..."Yahoo リアルタイム検索を利用した Twitter/X のスクレイピングでは、正確な投稿時刻を取得できません。そのため、投稿時刻を基準としたグループ化は行わず、テキスト内容のみでグループ化しています。
- アカウント設定はローカルストレージに平文 JSON で保存されるため、端末共有や XSS に対して漏洩リスクがある。暗号化対応が今後の課題。
- API キーやシークレットは環境変数で管理
- クロスオリジン対策として
/backend/netlify/functions/cors_proxy.jsを利用
- 現状は
/backend/test.httpを使った手動テストのみ - 自動テストスイートは未整備
- Netlify へデプロイ
- 設定は
/backend/netlify.tomlを参照
- Misskey への投稿は未対応(検討余地あり)
- ローカルストレージに保存した接続情報が平文のまま保持される(暗号化未対応)
- Mastodon 以外のサーバーへの認証情報テンプレートが未整備(環境変数を追加すれば拡張可能)
See LICENSE .