Banana Post 構築手順書

■ 概要

Discord予約投稿システム「Banana Post」

Discordの指定チャンネルに、テキスト・画像・動画・ZIPファイルを指定日時に自動送信できるWebシステムです。PHPとMySQLが使えるレンタルサーバーにインストールして使用します。

▼ 主な機能

・テキスト・画像（JPG/PNG/WebP）・動画（MP4/MOV）・ZIPファイルの予約投稿（本文または添付ファイルのいずれかがあれば登録可能）
・複数のDiscord Webhookを管理・切り替え
・Webhookごとに予約投稿の初期表示時刻を設定可能
・よく使う時刻を「時刻プリセット」として登録し、ワンタップで反映
・送信先チャンネル選択時、空いている日時を自動で提案（重複があれば自動で翌日以降にずらす）
・25MB超のファイルは自動分割送信
・添付ファイルのサムネイルをクリックすると拡大プレビュー表示（画像・動画とも対応）
・送信済み投稿の本文をDiscord上でリアルタイム編集
・ロールメンション対応（<@&ロールID> 形式）
・複数ユーザー管理（管理者・一般ユーザー）
・送信完了・失敗時のメール通知（編集URL・ダッシュボードURL付き）
・操作ログ・ログイン試行ログ

▼ 動作環境

・PHP 8.0以上（8.3推奨）
・MySQL 5.7以上（utf8mb4対応）
・Cronが使えるレンタルサーバー（毎分実行推奨）
・エックスサーバー スタンダードプラン以上での動作確認済み

※この手順書はエックスサーバーを前提とした手順で記載しています。他のレンタルサーバーをご利用の場合は、フォルダパス・Cron設定方法・PHPのフルパスなどをご利用環境に合わせて読み替えてください。


■ はじめに

この手順書は、Discord予約投稿システムをエックスサーバ上に構築するための手順をまとめたものです。PHPとMySQLが使えるレンタルサーバであれば他のサービスでも応用できますが、この手順書はエックスサーバ（スタンダードプラン以上）を前提としています。

※このシステムはサンプルとして「Banana Post」という名前が設定されています。config.phpのAPP_NAMEを変更することで任意の名前に変更できます。詳しくはステップ3を参照してください。

所要時間の目安は1〜2時間程度です。


■ Discordの仕様について

▼ 対応ファイル形式

このシステムで添付できるファイル形式は以下の通りです。

・画像：JPG / JPEG / PNG / WebP
・動画：MP4 / MOV
・その他：ZIP

動画ファイル（MP4・MOV）はDiscordに埋め込み形式で送信されます。受信者はDiscord上でそのまま再生できます。動画も画像・ZIPと同様に25MBの制限が適用されます。

Discord上でその場再生（埋め込み再生）を確実に行うには、動画のエンコード形式を **H.264** にすることを推奨します。H.265（HEVC）でエンコードされた動画はDiscordで再生できない場合があります。スマートフォンで撮影した動画は H.265 でエンコードされていることが多いため、アップロード前にエンコード形式をご確認ください。


▼ 添付ファイルのプレビュー表示

新規投稿・編集画面ともに、添付ファイル一覧のサムネイルをクリックすると画面中央に拡大表示されます。画像はそのまま拡大表示され、動画は再生コントロール付きで自動再生されます。表示を閉じるには、右上の「×」ボタンを押すか、画面の暗い部分をクリックするか、Escキーを押してください。


▼ 投稿内容のルール

投稿テキスト（本文）と添付ファイルは、どちらか一方が入力されていれば投稿を登録できます。両方とも空の場合はエラーになります。画像や動画のみを送りたい場合、本文を空欄のまま登録して問題ありません。


▼ ファイル添付の制限

Discord Webhookでは、1ファイルあたりの上限・1メッセージあたりの合計上限ともに最大25MBです。これはDiscordのプラン（無料・Nitro等）に関わらず共通の制限です。そのため、このシステムのWebhook登録画面でも上限は最大25MBまでしか設定できません。

この制限はDiscordがWebhook送信に課しているものであり、受信者側のプランには関係ありません。例えばNitroユーザーは通常50MBのファイルを扱えますが、Webhook経由での送信は25MB以下に制限されます。

1ファイルあたりのサイズ上限の目安：
・Discord無料：10MB
・Nitro Basic・Nitro：Webhook経由では最大25MB

Webhook登録時はご自身の利用状況に合わせて上限を設定してください。上限を超えるファイルは投稿フォームで警告が表示され、登録できません。

なお合計が25MBを超える場合は自動的に複数メッセージに分割して送信されます。分割される場合は投稿フォームに「〇件のメッセージに分割して送信されます」という案内が表示されます。例えばNitroユーザーが1ファイル20MBのファイルを2個添付した場合、合計40MBとなるため自動的に2件のメッセージに分割されます。

1ファイル自体が25MBを超える場合は分割できないため送信に失敗します。Webhook登録時のファイルサイズ上限は必ず25MB以下に設定してください。

分割送信された場合、Discordでは複数のメッセージとして連続投稿される形になります。画像の表示位置がメッセージ内で自動的に上部に移動するなど、意図した順番と異なる表示になる場合があります。


▼ ロールメンションについて

Discord上でテキストを直接入力する場合、`@ロール名` と入力するとメンションとして機能します。しかしWebhook API経由で送信する場合、`@ロール名` はただの文字列として扱われ、メンションとして機能しません。

Webhook経由でメンションを機能させるには以下の形式が必要です。

```
<@&ロールID>
```

例：`<@&123456789012345678>` と送信すると、Discord上では `@チョコバナナ会員` としてメンションが機能します。

ロールIDはDiscordのサーバー設定→「ロール」→該当ロールを右クリック→「IDをコピー」で取得できます。

このシステムでは投稿テキストをそのままDiscordに送信するため、投稿フォームに `<@&ロールID>` 形式で入力することでメンションが機能します。定型文を生成するツールをお持ちの場合は、そちらで `@ロール名` を `<@&ロールID>` に変換してからペーストすることをお勧めします。

▼ 送信済み投稿の本文編集

送信済みの投稿はダッシュボードの「送信済み」タブから本文のみ編集できます。「本文編集」ボタンをクリックすると編集画面が開き、Discordの投稿内容をリアルタイムで更新できます。

ただし以下の制約があります。

・テキスト本文のみ変更可能です。添付ファイルの変更はできません。
・分割送信された場合は最初のメッセージのみ編集できます。
・このシステムで送信した投稿のみ対象です。以前に送信済みでメッセージIDが保存されていない投稿は編集できません。
・Webhookが削除されている場合は編集できません。

万が一メッセージIDが保存されていない投稿がある場合は、Discordのメッセージを右クリック→「IDをコピー」で取得したIDを「ID登録」ボタンから手動登録することで本文編集が可能になります。


▼ Webhookの並び替えと初期選択

Webhook管理画面では登録済みのWebhookをドラッグ＆ドロップで並び替えできます。一番上に配置したWebhookが新規投稿フォームの初期選択になります。よく使うチャンネルを一番上に置いておくと便利です。

並び替え後は「この順番で保存する」ボタンを押すまで保存されません。ボタンを押し忘れないよう注意してください。

登録済みのWebhookは「編集」ボタンから表示名・Webhook URL・ファイルサイズ上限を変更できます。URLを間違えて登録した場合などに利用してください。


▼ Webhookごとの初期表示時刻

Webhookの新規登録・編集画面で「予約投稿の初期表示時刻」を設定できます。ここで設定した時刻は、新規投稿フォームでそのWebhookを選んだときの送信時刻の初期値として使われます。「このチャンネルはいつも夜9時に投稿する」といった運用の場合に設定しておくと便利です。

未設定のままにした場合は、現在時刻から30分単位で切り上げた時刻が自動的に使われます（例：14:12なら14:30、14:45なら15:00）。


▼ 時刻プリセット

新規投稿フォームの送信日時欄には、よく使う時刻をボタンとして登録しておける「時刻プリセット」機能があります。プリセットボタンをクリックすると、その時刻（時・分）だけが即座に反映されます（日付は変わりません）。

プリセットの登録・削除は「✎ プリセット編集」ボタンから行います。

・「＋ 追加」：新しい時刻をプリセットとして登録します（最大10個まで）
・プリセットボタンをクリック：その場で時刻を編集できます（インライン編集）
・「×」：そのプリセットを削除します
・「✓ 完了」：編集モードを終了します

プリセットはユーザーごとに管理されており、他のユーザーが登録したプリセットが表示されることはありません。


▼ 送信先チャンネル選択時の日時自動調整

新規投稿フォームで送信先チャンネル（Webhook）を選択すると、送信日時が自動的に以下のルールで提案されます。

1. 選択したWebhookの「初期表示時刻」（未設定の場合は現在時刻から30分単位で切り上げた時刻）を基準にする
2. その日時が既に過去、または同じWebhook宛に同じ日時の予約が既にある場合は、1日ずつ先へずらして空いている日時を探す
3. 見つかった日時が自動的に入力される

そのため「チャンネルを選び間違えていた」という場合でも、選び直すだけで送信日時も適切な空き日時に調整されます。この自動調整はチャンネルを選択・変更したタイミングでのみ動作し、プリセットボタンをクリックしたときには動作しません（プリセットは時刻のみを反映します）。

予約投稿の編集画面でチャンネルを変更した場合も同様に日時が自動調整されますが、編集中の投稿自身は重複判定の対象から除外されます。


■ 必要なもの

・エックスサーバのアカウント（契約済みであること）
・Discordのアカウントと投稿先チャンネルのWebhook URL
・FTPクライアントまたはエックスサーバのファイルマネージャー
・phpMyAdminへのアクセス


■ ステップ1：フォルダの準備

▼ Web公開外フォルダの作成

アップロードされたファイルはWeb公開ディレクトリの外に保存します。これにより、URLを直接入力してもファイルにアクセスできない安全な状態になります。

エックスサーバのファイルマネージャーを開き、ホームディレクトリ（/home/サーバーID/）の直下に以下の2つのフォルダを作成してください。

・upload_tmp（一時保存用）
・upload_archive（送信済みファイルの保管用）

【重要】両フォルダのパーミッションは必ず700に設定してください。ファイルマネージャーで右クリック→パーミッション変更から設定できます。700はオーナーのみアクセス可能な設定です。

フォルダの場所のイメージ：

/home/サーバーID/
├── upload_tmp/        ← ここに作成（パーミッション700）
├── upload_archive/    ← ここに作成（パーミッション700）
└── ドメイン名/
    └── public_html/   ← Webから見えるのはここだけ


▼ システムフォルダの作成

次に、public_html内にシステム用のフォルダを作成します。フォルダ名は推測されにくい名前を推奨します。この手順書では「banana_post」を例として使用します。

/home/サーバーID/ドメイン名/public_html/banana_post/

このフォルダのパーミッションは705のままで問題ありません。


■ ステップ2：MySQLデータベースの作成

▼ データベースの作成

エックスサーバのサーバーパネルにログインし、「MySQL設定」からデータベースを新規作成してください。

設定値：
・文字コード：UTF-8（utf8mb4）を選択（重要）
・データベース名、ユーザー名、パスワードは任意

【注意】文字コードはUTF-8（utf8mb4）を必ず選んでください。絵文字や日本語ファイル名を正しく扱うために必要です。通常のUTF-8（utf8）では4バイト文字が扱えないため問題が発生することがあります。


▼ テーブルの作成

phpMyAdminを開き、作成したデータベースを選択して「インポート」タブから以下のSQLファイルをインポートしてください。

ファイル：discord_scheduler.sql

インポートが成功すると以下の7テーブルが作成されます。

・users（ユーザー管理）
・webhooks（Webhook管理・並び替え順・初期表示時刻含む）
・time_presets（時刻プリセット管理）
・scheduled_posts（予約投稿）
・post_files（添付ファイル）
・activity_logs（操作ログ）
・login_attempts（ログイン失敗記録）


▼ 管理者ユーザーの作成

同梱の init_admin_ダミー.sql を init_admin.sql にリネームしてください。

続けてphpMyAdminの「インポート」から以下のSQLファイルをインポートしてください。

ファイル：init_admin.sql

このSQLにはデフォルトのパスワードが含まれています。インポート後、必ずシステムの設定画面からパスワードを変更してください。


■ ステップ3：設定ファイルの編集

同梱の config_ダミー.php を config.php に、db_ダミー.php を db.php にそれぞれリネームしてください。以降の手順はリネーム後のファイルを編集します。

▼ config/db.php の編集

以下の項目をステップ2で作成したデータベースの情報に書き換えてください。

・DB_HOST：MySQLのホスト名（エックスサーバのMySQL情報画面で確認）
・DB_NAME：データベース名
・DB_USER：MySQLユーザー名
・DB_PASS：MySQLパスワード

エックスサーバのMySQLホスト名はサーバーパネル→「MySQL設定」→「MySQL情報」タブで確認できます。mysql○○○○○.xserver.jpという形式です。


▼ config/config.php の編集

以下の項目を環境に合わせて書き換えてください。

・BASE_URL：システムのベースURLです。メール通知に含まれるリンクの生成に使用します。末尾のスラッシュは不要です。
  例：define('BASE_URL', 'https://ドメイン名/banana_post');

・APP_NAME：画面上に表示されるシステム名です。任意の名前に変更できます。フォルダ名やCronのパスは別途変更が必要です。
  例：define('APP_NAME', '自分のシステム名');

・UPLOAD_TMP_DIR：ステップ1で作成したupload_tmpの絶対パス
  例：/home/サーバーID/upload_tmp/

・UPLOAD_ARCHIVE_DIR：ステップ1で作成したupload_archiveの絶対パス
  例：/home/サーバーID/upload_archive/

・ENCRYPT_KEY：Webhook URLの暗号化に使用する秘密鍵。以下のコマンドで生成できます。
  python3 -c "import secrets; print(secrets.token_hex(32))"
  または任意の64文字の16進数文字列を使用してください。絶対に外部に公開しないでください。

【重要】ENCRYPT_KEYは一度設定したら変更しないでください。変更するとDBに保存済みのWebhook URLが復号できなくなります。


■ ステップ4：ファイルのアップロード

▼ ファイル構成とパーミッション

以下の構成でファイルをアップロードしてください。

banana_post/                （705）
├── index.php               （604）
├── logout.php              （604）
├── favicon.ico             （604）
├── admin/                  （705）
│   ├── dashboard.php       （604）
│   ├── footer.php          （604）
│   ├── header.php          （604）
│   ├── post_edit.php       （604）
│   ├── post_new.php        （604）
│   ├── settings.php        （604）
│   ├── thumb.php           （604）
│   ├── users.php           （604）
│   └── webhook.php         （604）
├── config/                 （705）
│   ├── .htaccess           （604）← Webからのアクセスを禁止
│   ├── config.php          （604）
│   └── db.php              （604）
├── cron/                   （705）
│   └── post.php            （701）← 実行権限が必要
└── logs/                   （777）← PHPからの書き込みに必要
    └── .htaccess           （604）← Webからのアクセスを禁止

【重要】cron/post.phpのパーミッションは701にしてください。604のままではCronから実行できません。これはエックスサーバの仕様です。

【重要】config/.htaccessとlogs/.htaccessは必ずアップロードしてください。これらがないとブラウザから設定ファイルやログに直接アクセスできてしまいます。


■ ステップ5：Cronの設定

▼ Cronの設定方法

エックスサーバのサーバーパネルから「Cron設定」を開き、以下の内容でCronを追加してください。
詳細はエックスサーバの公式マニュアルも参照してください。
https://www.xserver.ne.jp/manual/man_program_cron.php

・分：*
・時間：*
・日：*
・月：*
・曜日：*
・コマンド：/usr/bin/php8.3 /home/サーバーID/ドメイン名/public_html/banana_post/cron/post.php

【重要】コマンドはPHPのフルパスで指定する必要があります。「php」だけでは動作しません。PHPのフルパスはサーバーパネル→「サーバー情報」→「コマンドパス一覧」で確認できます。PHPのバージョンはサイトで使用しているバージョンに合わせてください。

【重要】コマンドのパスにはドメイン名とpublic_htmlが含まれることに注意してください。
正しい例：/usr/bin/php8.3 /home/サーバーID/ドメイン名/public_html/banana_post/cron/post.php
間違いの例：/usr/bin/php8.3 /home/サーバーID/banana_post/cron/post.php

【補足】毎分実行のCronはサーバー負荷の警告が表示されますが、このスクリプトは軽量な処理のため実用上問題ありません。


▼ Cronの動作確認

Cronが正しく動作しているか確認するために、簡単なテストを行うことを推奨します。

1. cron/フォルダにcron_test.phpを作成し、パーミッションを701に設定します。
2. Cronのコマンドを一時的にcron_test.phpのパスに変更します。
3. 2〜3分後にlogs/フォルダにcron_test.logが生成されることを確認します。
4. 確認できたらCronのコマンドを元のpost.phpに戻します。

cron_test.phpの内容：

<?php
$log_file = '/home/サーバーID/ドメイン名/public_html/banana_post/logs/cron_test.log';
$msg = '[' . date('Y-m-d H:i:s') . '] Cronテスト実行OK' . PHP_EOL;
file_put_contents($log_file, $msg, FILE_APPEND);

【補足】Cron結果の通知アドレスにメールアドレスを設定しておくと、エラーが発生した時にメールで通知されるため、トラブルシューティングに役立ちます。

【重要】Cronが動作しない場合、通知メールアドレスを設定してもメールが届かないことがあります。メールが届かない場合はCron自体が実行されていない可能性が高く、設定内容（コマンドのパス・PHPのフルパス・ファイルのパーミッション）を見直してください。メールはCronが実行されて初めて送信されるため、Cronが動いていなければメールも届きません。

Cronが動作しない場合は以下を順番に確認してください。

1. cron/post.phpのパーミッションが701になっているか
2. Cronのコマンドにドメイン名とpublic_htmlが含まれているか
3. PHPのフルパスが正しいか（サーバーパネルのコマンドパス一覧で確認）
4. Cron設定の「設定状況」がONになっているか
5. Cron結果の通知アドレスを設定してエラーメールを確認する


■ ステップ6：動作確認

▼ ログインの確認

ブラウザで以下のURLにアクセスしてログイン画面が表示されることを確認してください。

https://ドメイン名/banana_post/

init_admin.sqlに記載されているユーザー名とパスワードでログインできることを確認したら、すぐに設定画面からパスワードを変更してください。


▼ テスト投稿の実施

以下の手順でテスト投稿を行い、一連の動作を確認してください。

1. Webhook管理画面でDiscordのWebhook URLを登録する
2. 新規投稿画面で2〜3分後の日時を指定してテキストのみの投稿を登録する
3. 指定した時刻にDiscordに投稿が届くことを確認する
4. ダッシュボードでステータスが「送信済み」になることを確認する
5. logs/cron.logに送信完了のログが出力されることを確認する


■ トラブルシューティング

▼ ファイルがアップロードできない

エックスサーバのPHPデフォルト設定ではアップロードサイズに制限がある場合があります。public_html直下の.user.iniファイルに以下を追記することで上限を変更できます。

upload_max_filesize = 50M
post_max_size = 60M


▼ 日本語ファイル名が文字化けする

DBの文字コードがutf8mb4になっているか確認してください。phpMyAdminでテーブルの照合順序がutf8mb4_unicode_ciになっていれば正常です。


▼ 送信はされているがDiscordにアイコンや名前が表示されない

短時間に同じWebhookから複数の投稿があると、Discordが自動的にメッセージをグループ化し、最初のメッセージにのみアイコンと名前を表示します。実際の運用では間隔が空くため通常は表示されます。


■ セキュリティについて

▼ 定期的に確認すること

・logs/cron.logに異常なログがないか確認する
・activity_logsテーブルで不審な操作がないか確認する
・login_attemptsテーブルで大量のログイン試行がないか確認する

▼ パスワードの管理

・初期パスワードは必ず変更してください
・ユーザーを追加した際は初期パスワードを安全な方法で伝え、ログイン後すぐに変更してもらってください
・config/db.phpとconfig/config.phpは絶対に公開しないでください


■ ファイル一覧

同梱されているファイルの一覧です。ファイル名が「_ダミー」で終わるものは、リネームしてから使用してください。PHPファイルはステップ4のファイル構成通りにアップロードしてください。

SQLファイル：
・discord_scheduler.sql（テーブル作成・初回のみ使用）
・discord_scheduler_reset.sql（初期化用・やり直し時のみ使用）
・init_admin_ダミー.sql（管理者ユーザー作成用。init_admin.sql にリネームしてから使用）

設定ファイル：
・config_ダミー.php（config.php にリネームしてから使用。ステップ3参照）
・db_ダミー.php（db.php にリネームしてから使用。ステップ3参照）
