画像からカレンダーに予定を登録するdiscord botを作る
TODO
(リファクタリング)OpenAIのクライアントを使う
(リファクタリング)Chat completions APIを使わずResponses APIを使う
jsいらない
(bug) スレッドでの返答時に最初の回答が残る
dataから読み取る仕組みをやめてDOからのみ読み取る
事前にDOにタスクをアップロードする
action idをUUIDにする
データの変換がうまくいっていないものがあるのを直す
読み込みロジックをレビューする
全体を読み込んだりしていると時間と金の無駄
GPT-5.icon TaskManagementService.getActualTasksData() が TaskLoaderService.loadAllTaskData() を呼んでおり、すべての JSONL を DO から読むため、時間とコストが増大します。
2025-09-19
深い分析を実装するのは非常に大変そうなので、そういう相談はClaudeなりChatGPTのインタフェースが使えるところでやると割り切る
このツールでは
すでに作られたタスク管理の進捗管理ができる
ある程度の分解はやる
PCがなくてもタスクにアクセスできる
に限り、問題の分析はほどほどで妥協してもよいこととする
この制限がある理由
Chatをいい感じに実装するのは考えることが多くてめんどくさそう。品質保証も大変そう
現状のアーキテクチャではslash commandしか使えないが、問題をブレークしていくときに毎回スラッシュを打つのがだるい。でもそうしたら常駐botを作る必要がある。まだノウハウを持っていないから時間がかかる。ホスティングも考えなければいけない
iOS 26#68ca35ff0000000000a5c98aが日本語対応していないので作る
左のメニューから「Bot」をクリック → 「Add Bot」を押してBotユーザーを作成
Botページに表示される「Reset Token」からトークンを発行
コードで使う
左メニューの「OAuth2」→「URL Generator」
Scopesで「bot」を選択
https://gyazo.com/e51771eaf07d12ca79f5738bc0d9c48c
GPT-5.iconBotに必要なのは最低限以下です。
View Channels
Send Messages
Send Messages in Threads(スレッドで使う場合)
Read Message History
Add Reactions(必要なら)
もし今後「/schedule」みたいなスラッシュコマンドを使いたいなら、Use Slash Commands にもチェックを入れておくと便利です。
MESSAGE CONTENT INTENTをONにした基素.icon
発行したURLがらサーバーに追加
Claude Sonnet 4.icon
Google Calendar API の取得
1. Google Cloud Consoleにアクセス
https://console.cloud.google.com/
2. プロジェクト作成
新しいプロジェクトを作成または既存を選択
3. Google Calendar API有効化
「有効なAPIとサービス」でGoogle Calendar APIを有効にする
4. 認証情報作成
APIs & Services > Credentials
Create Credentials > OAuth 2.0 Client IDs
Application type: Desktop application
名前を入力(例:Discord Calendar Bot)
5. credentials.jsonダウンロード
作成した認証情報のDownload JSONをクリック
ファイル名をcredentials.jsonに変更
プロジェクトルートに配置
OpenAI API Key の取得
3. API Keysページに移動
https://platform.openai.com/api-keys
4. Create new secret keyをクリック
名前を付ける(例:discord-calendar-bot)
キーをコピー(一度しか表示されません)
5. 課金設定
GPT-5-nanoを使用するには課金が必要
Billingで支払い方法を設定
文字列でできた
https://gyazo.com/72c57c1381587672fd903169ae06282f
スクショ画像を圧縮すると実用的かつ安くなるか実験
ホスティングどこでやるか問題
スリープするサービスの場合UptimeRobotで起こす
常駐させられて無料で使えてサーバーの面倒も見なくてもいいサービスがない
Cloudflare Workersでもできるか調べたらできそうなのでこれでいく。ただしWebSocketは使えない。
gpt-5-nanoのレスポンスが正しいJSONを返さないのでstructured outputsを使った
discordアプリのprivate化
discordの追加情報が必要っぽい
DISCORD_PUBLIC_KEY(必須)
使い道: DiscordのInteractions署名(Ed25519)を検証するためにCloudflare Workers側で使用。
取得: Discord Developer Portal → 対象アプリ → General Information → Public Key
DISCORD_BOT_TOKEN(必須)
使い道: BotがDiscord APIへ呼び出すための認証(Botヘッダー)。リフレッシュトークンではありません。
取得: Developer Portal → Bot → Token(Reset Token/Copy)
注意: 流出したら必ず「Reset Token」して無効化してください。
DISCORD_APPLICATION_ID(必須)
使い道: コマンド登録スクリプトやOAuth2招待URLの生成など(Client IDと同じ値)。
取得: Developer Portal → General Information → Application ID(= Client ID)
code:zsh
npx wrangler secret list --env production
[
{
"name": "DISCORD_APPLICATION_ID",
"type": "secret_text"
},
...
これをやる必要があった
discord側のコマンド定義
nodeでdiscordに直接定義
サーバーサイドの実装
サーバーにデプロイ
DiscordのInteractions APIとWebSocket (Gateway)
Discord interaction は3秒でタイムアウト - Discord
APIの制限で、interactionレスポンスは3秒以内に返す必要があります
基素.iconこの制約がきつくてダメそうだな....
Gemini 2.5 Pro.icon
はい、その通りです。Discordのインタラクション(スラッシュコマンドやボタンなど)は、3秒以内に最初の応答を返さないとタイムアウトしてしまいます。時間がかかる処理を直接実行しようとすると、「このインタラクションは失敗しました」というエラーがユーザーに表示されてしまいます。
この問題を回避し、時間のかかる処理を実行するには、応答を遅延(defer)させるのが一般的な方法です。
長時間かかる処理の解決策
主な解決策は、とりあえず「処理中です…」というメッセージを先に表示しておき、実際の処理が終わった後でそのメッセージを編集するという流れです。これを**遅延応答(Deferred Response)**と呼びます。
1. 応答を遅延させる (とりあえず返事をする)
まず、インタラクションを受け取ったらすぐに「考え中…」や「処理中…」といったメッセージをDiscordに送ります。これにより、3秒のタイムアウトを回避できます。
discord.py の場合: interaction.response.defer() を呼び出します。
discord.js の場合: interaction.deferReply() を呼び出します。
このコマンドを送ると、Discordのチャット欄には「(ボット名) が考え中…」というメッセージがユーザーに表示されます。
2. 時間のかかる処理を実行する
遅延応答を返したら、その後に時間のかかる処理(例: 外部APIへの問い合わせ、データベースの重いクエリ、画像の生成など)を実行します。遅延させておけば、この処理に3秒以上かかっても問題ありません。
3. フォローアップで最終的な結果を送信する
処理が完了したら、最初に応答した「考え中…」のメッセージを編集して、最終的な結果をユーザーに表示します。これを**フォローアップメッセージ(Follow-up Message)**と呼びます。
discord.py の場合: interaction.followup.send() や interaction.edit_original_response() を使ってメッセージを送信または編集します。
discord.js の場合: interaction.followUp() や interaction.editReply() を使います。
コードの具体例 (discord.py)
code:python
import discord
import asyncio
# discord.pyの基本的なBot設定は省略
@bot.tree.command(name="long_task", description="時間のかかる処理を実行します。")
async def long_task(interaction: discord.Interaction):
# 1. 応答を遅延させる
await interaction.response.defer()
# 2. 時間のかかる処理(ここでは5秒待つ処理をシミュレート)
await asyncio.sleep(5)
# 3. 処理完了後、フォローアップでメッセージを送信(または編集)
await interaction.followup.send("お待たせしました!処理が完了しました。🎉")
このコードを実行すると、スラッシュコマンドを叩いた直後に「考え中…」と表示され、5秒後に「お待たせしました!…」というメッセージに置き換わります。
まとめ
table:_
手順 概要 目的
1. Defer (遅延) とりあえず「処理中」と応答する 3秒のタイムアウトを回避する
2. Process (処理) 時間のかかる処理を実行する 本来の目的のタスクを完了させる
3. Follow-up (追従) 処理結果で元のメッセージを更新する ユーザーに最終的な結果を伝える
この「とりあえず応答して、後から編集する」という流れが、Discordのインタラクションで長時間かかる処理を扱う際の基本となります。
基素.iconいけるんか!
(なんやかんや)
GPT-5.icon
確認した実装の根拠
/koyomi-setup:
認証済みかをユーザーID単位でチェックし、未認証なら OAuth 認可URLを生成して案内。
OAuth フロー:
認可コードを /oauth/callback で受け取り、アクセストークン/リフレッシュトークンに交換して保存。
有効期限切れ時は refresh_token で再取得(ユーザー単位で更新&保存)。
イベント作成:
/koyomi では UserTokenStorage でユーザーの有効トークンを取得。
GoogleCalendarOAuthService は Bearer トークン(ユーザーのアクセストークン)で Calendar API を叩く。
つまり「セットアップした本人のカレンダー(デフォルトは primary)」に作成されます。
気になる点・改善提案
資格情報の種類の不整合:
OAuth 実装は Google の「OAuth クライアント(installed/web)」の client_id/client_secret を想定。
一方でドキュメントにはサービスアカウント手順が残っており混同の恐れあり。デプロイ時の GOOGLE_CREDENTIALS は「OAuth クライアントのJSON」に統一してください(サービスアカウントJSONは不可)。
リダイレクトURIがハードコード:
OAuth の redirect_uri が固定URLになっています。環境ごとに変えられるように環境変数(例: OAUTH_REDIRECT_URL)にすることを推奨。
state の安全性:
state に Discord の userId をそのまま入れています。CSRF/なりすまし対策として、ランダムなノンスを含めて DO/DB に関連付け、短時間で失効させる仕組みを追加するとより安全です。
メッセージに「10分間有効」とありますが、実際の有効期限管理は現状実装されていません。ノンス+有効期限での検証を追加推奨。
GPT-5.iconstate に公開の Discord userId だけを使うと、攻撃者がその userId を使って OAuth コールバックを先取り・偽装でき、アカウント連携の乗っ取り(ログイン乗っ取りや通知/データの窃取)や CSRF・リプレイのリスクが生じます。対策はランダムノンスを発行して DB/DO に紐づけ、短時間で失効させることです。
以下、代表的な攻撃パターンをステップで示します(被害の種類ごとに短くまとめます)。
攻撃パターン A:アカウント連携の乗っ取り(最も典型)
1. 攻撃者はターゲットの Discord userId を把握する(公開情報や推測で容易)。
2. 攻撃者は自分の Discord アカウントで OAuth の承認を行う(Discord 側で承認ボタンを押す)。
3. 攻撃者は OAuth の authorize URL に state=<victim_userId> を付けてリダイレクト先(あなたの callback エンドポイント)へ戻らせる。
4. あなたのサーバは受け取った state を元に「victim_userId のアカウント」と認識し、Discord の access_token / Discord ユーザ情報(=攻撃者の情報)をそのアカウントに紐づける。
5. 結果:攻撃者の Discord アカウントが victim のサイトアカウントに紐づく。サイトが Discord OAuth を使ってログインできる設計なら、攻撃者は victim としてログインできる(完全乗っ取り)。
攻撃パターン B:CSRF 的に行われる権限付与(機能悪用)
1. 攻撃者が state=<victim_userId> を使った OAuth リクエストを生成し、victim にクリックさせる(メールやチャットで誘導)。
2. victim がクリックして Discord 承認をすると、callback に state=<victim_userId> が戻るがサーバ側で「session を照合する」実装が無い/甘いと、そのコールバック処理で攻撃者側の意図したデータ変更(連携設定、通知先変更など)が victim のアカウントに適用される。
3. 結果:victim のアカウント設定を書き換えられたり、通知を攻撃者側へ流すようにできる。
攻撃パターン C:リプレイ/セッション固定(state を再利用されるケース)
1. state を一定期間再利用可能にしていると、攻撃者が過去に取得した callback 情報(code)や既知の state を再送して再度連携を成立させる可能性がある。
2. 古い state に紐づく処理が上書きされると、期待しないタイミングで連携が切り替わる/乗っ取られる。
具体的には
前提(OAuth 標準でよくある流れ)
1. クライアントが OAuth 認可要求を作る: GET /authorize?client_id=…&redirect_uri=…&state=XYZ
2. ユーザがプロバイダで承認 → プロバイダは redirect_uri に code と state を付けてリダイレクトする: GET https://app/callback?code=CODE123&state=XYZ
3. サーバは code を使って( token エンドポイントへ POST して) access_token を得る。
code は通常一度しか使えないが、state はサーバの扱い次第で再利用可能になる
攻撃者が入手できるもの:過去に発行された state(=ノンス)、対応する code、あるいは callback の完全な URL。
入手経路の例:被害者のブラウザ履歴/プロキシログ/リファラ漏洩/アクセスログの誤出力/開発者が誤って共有したスクリーンショット/中間者(MITM)/フィッシングメールで被害者を誘導して生成させたもの
1. 攻撃者がある時点で callback URL を取得する: https://app/callback?code=CODE123&state=STATEabc
2. 取得時点でサーバが state の有効期限チェックや「一度使ったか」チェックをしていなかった(または有効期間内に再利用を許していた)。
3. 攻撃者は後で同じ callback をサーバへ送る(ブラウザで直接アクセス、curl、POST など)。例: GET https://app/callback?code=CODE123&state=STATEabc
4. サーバ側が state を信じて「STATEabc に紐づくユーザ」を参照し、code を token エンドポイントへ送ってアクセストークンを取得、あるいは内部で連携フラグを更新する。
5. 結果:攻撃者は(再)連携を成立させたり、access_token を取り戻して不正に API を呼べる、または被害者側の連携先を攻撃者アカウントに差し替えられる。
攻撃パターン D:情報収集/プライバシー侵害(バリエーション)
1. 攻撃者は victim の userId を使って大量の OAuth リクエストを送る。
2. サーバがエラーログや失敗レスポンスで敏感情報(内部のユーザID存在有無など)を返すと、ユーザ存在確認や挙動プロファイリングに使える。
3. 結果:ターゲットの存在確認、タイミング攻撃、次段のフィッシングに利用される。
倫理的にグレー/悪用バリエーション(含めます)
連携を悪用して victim の DM やサーバ情報を読み取れる API 権限がある場合、メッセージ窃取に使える。
サービスが Discord を介して二要素やリカバリ手段にしている場合、その回復手段を乗っ取りに悪用できる。
大量に同様の state を試すことでアカウントの存在・設定の有無をスキャンする(プライバシー侵害・偵察)。
「ランダムノンス+DB紐付け+短期失効」が効くか(簡潔に)
ノンスは予測不能なので攻撃者は正しい state を作れない。
サーバでノンスとユーザの session/cookie を照合すれば、外部からの偽コールバックを拒否できる。
期限を短くするとリプレイ可能性がほぼ消える。
実装チェックリスト(推奨、短く)
state にランダム128ビット以上のノンスを入れる(userId は含めないか別フィールドで暗号化して持つ)。
ノンスを DB/DO に保存し、callback 時に一致・未使用・期限内か検証する。
callback 時にブラウザのセッションID/ログイン状態とノンスを照合する。
ノンスは一度使ったら無効化(使い切り)。
可能なら OAuth の PKCE を併用する/最小権限のスコープに限定する。
トークン保存のセキュリティ:
Durable Object に平文保存です。可能なら暗号化(KMS 相当がないため、Workers Secrets のキーでAES暗号化等)を検討してください。少なくとも取り消し/削除コマンド(/koyomi-disconnect)の用意を。
✅ 実装した機能
1. 純粋なランダムstate
256ビットの暗号学的ランダムノンス
userIdは別途Durable Objectで安全に管理
2. OAuth 2.1 PKCE対応
手動PKCE実装(SHA256)
code_verifier/code_challengeの生成・検証
3. AES-GCM暗号化
Workers Secretsを使用した暗号化キー管理
access_token と refresh_token の暗号化保存
4. トークン管理
/koyomi-disconnect コマンドでトークン削除
使用済みstateの即座無効化
10分間の適切な有効期限
カレンダー選択:
現状 calendarId は "primary" 固定です。ユーザーが別カレンダーを指定できる設定(ユーザー単位で calendarId を保存)を追加すると運用が楽になります。
この為にわざわざsetupを用意した基素.icon
リダイレクトURIと Google コンソール:
Google コンソール側の承認済みリダイレクトURIに、実際の Workers のURLを必ず登録してください。異なる環境での動作時は要更新。
しないと使えないのでしてる基素.icon
https://gyazo.com/79e58d67efccd24fe84206f54141309b
Discordのslash commandは必ず「事前定義されたオプション」にしか値を入れられない
現状の仕様
GPT-5.icon
短答:
現状の実装では「画像とテキストを同時に渡した場合」、テキスト解析が優先され、画像は無視されます。つまり、画像に対してテキストでフォローアップする動作にはなっていません。
もう少し詳しく:
コマンドにテキストがあるとテキストだけを解析、テキストが無く画像があると画像だけを解析、という排他的なフローになっています。
画像解析時にモデルへ渡すテキストは固定のプロンプトで、ユーザーが入力した追記テキストは反映されません。
実現するには(方針例):
両方来た場合は「画像+ユーザーの追記テキスト」を同じリクエストでVisionモデルに渡す。
具体的には、画像解析メソッドに任意のuserTextを受け取り、messagesのcontentに固定プロンプト+userTextを並べて送る形にする。
あるいは、まず画像から候補イベントを出し、その後のユーザーメッセージ(フォローアップ)を受けて再解析・上書きするスレッド型のフローを追加する。
これをやりたい TODO
ご希望があれば、「両方渡せば画像に対する指示として解釈する」動作に変えるパッチ案を用意します。