telegram-bot-sessions について
Telegram bot 開発者向けに、grammY で会話状態を管理する方法を説明するスキルです。メモリ内セッション、SQLite による永続化、テスト間の状態分離を扱います。
主な機能
- 01grammY セッションと ctx.session の説明
- 02MemorySessionStorage の解説
- 03SQLite ストレージアダプターの説明
- 04セッション構造の設計ガイド
- 05テスト単位の状態分離の説明
活用シーン
- 01複数メッセージにまたがる会話フローの実装
- 02開発用・本番用ストレージの選択
- 03セッションデータとフィールド変更の設計
- 04テストケース間での bot 状態の分離
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ agntdev/ skills --skill telegram-bot-sessionsインストール後、Claude Code で/telegram-bot-sessionsを入力して読み込まれたか確認します。
詳細
telegram-bot-sessions Skill
Telegram ボットでユーザーの状態を永続化する方法 — その必要性、grammY による解決方法、ツールキットでのラップ方法を解説します。
agntdev パイプライン向けに作られています。 発見とタスク獲得の ループについては、agnt-cli-builder をご覧ください。 この Skill では、獲得したタスクの実装に組み込む MemorySessionStorage と SQLite アダプターの パターンを説明します。
1. セッションが必要な理由(Bot API)
Telegram Bot API はステートレスです。すべての Update は新しい HTTP リクエストとして届きます。ボットには、ユーザーが以前に何をしたかを記憶する組み込み機能がありません。
User: "I want to book"
Bot: "What service?"
User: "Haircut"
Bot: "When?"
User: "Tomorrow 2pm"
Bot: "Booked!"
状態がなければ、ボットは「Haircut」が「どのサービスをご希望ですか?」という質問への回答なのか、単なるメッセージなのかを判断できません。会話のどの段階にあるかを記憶できないのです。
Bot API での処理方法 — 組み込みの仕組みはありません
Bot API は Update を配信するだけです。以下の処理はボット側で行う必要があります。
- 誰がどの段階にいるかを記憶する
- メッセージ間で途中のデータを保存する
- フローが完了したら状態をクリアする
選択肢:データベース。 メッセージごとにユーザーの状態を SQLite/Redis に書き込みます。機能しますが、遅延と複雑さが増します。
選択肢:インメモリ Map。 高速ですが、再起動すると失われます。開発には適していますが、本番環境には不向きです。
選択肢:grammY セッション。 フレームワークがこれらの処理を抽象化します。
2. grammY セッション — フレームワークによる解決策
grammY は session() プラグインを提供します。このプラグインはチャットごとの状態を保存し、ctx.session として利用できるようにします。
基本的な設定
import { Bot, session } from "grammy";
interface SessionData {
step: string;
service?: string;
}
const bot = new Bot(token);
bot.use(session({
initial: (): SessionData => ({ step: "idle" }),
// storage: ... // defaults to in-memory Map
}));
bot.command("book", async (ctx) => {
ctx.session.step = "choosing_service";
await ctx.reply("What service?");
});
bot.on("message:text", async (ctx) => {
if (ctx.session.step === "choosing_service") {
ctx.session.service = ctx.message.text;
ctx.session.step = "choosing_time";
await ctx.reply("What time?");
}
});
セッションキー
grammY は chatId_userId("12345_67890")をキーとしてセッションを管理します。つまり、次のようになります。
- プライベートチャット: ユーザーごとに 1 つのセッション(チャットとユーザーは同一人物)
- グループチャット: チャット内のすべてのユーザーで 1 つのセッションを共有
- セッションはチャットごとに作られ、ユーザー全体に対して作られるものではありません
セッションの型 — ctx.session の型指定
import { session, type SessionFlavor } from "grammy";
type MyContext = Context & SessionFlavor<SessionData>;
const bot = new Bot<MyContext>(token);
bot.use(session({ initial: () => ({ step: "idle" }) }));
// ctx.session is now typed:
bot.command("book", async (ctx) => {
ctx.session.step = "choosing"; // TypeScript knows this field
});
StorageAdapter インターフェース
grammY セッションは、次のインターフェースを実装するあらゆるストレージバックエンドで動作します。
interface StorageAdapter<T> {
read(key: string): T | undefined;
write(key: string, value: T): void;
delete(key: string): void;
has(key: string): boolean;
readAllKeys(): string[];
}
組み込みの選択肢:
- デフォルト — インメモリの
Map(高速、再起動すると失われます。開発には適しています) - SQLite — 永続化され、再起動後も保持されます(
@grammyjs/storage-sqliteを追加) - Redis — 高速かつ永続化されます(
@grammyjs/storage-redisを追加) - Firebase、MongoDB、Supabase — コミュニティ製アダプターを利用できます
3. ツールキット(src/toolkit/)— セッションのデフォルト設定
MemorySessionStorage
ボットスターターテンプレートの src/toolkit/ に組み込まれたツールキットには、
Map をバックエンドとする grammY 互換の StorageAdapter、
MemorySessionStorage が含まれています。
import { MemorySessionStorage } from "../src/toolkit/storage/memory.js";
// Implements StorageAdapter:
const store = new MemorySessionStorage<SessionData>();
store.write("123_456", { step: "idle" });
store.read("123_456"); // { step: "idle" }
store.has("123_456"); // true
store.delete("123_456");
store.readAllKeys(); // []
通常、これを直接インスタンス化する必要はありません。 createBot() がデフォルトで使用します。
const bot = createBot<Session>(token, {
initial: () => ({ step: "idle" }),
// storage omitted → MemorySessionStorage used automatically
});
セッションの構造設計
セッションはフラットでシリアライズ可能な状態にしてください。関数、クラスのインスタンス、循環参照は含めないでください。
interface Session {
// Dialog state
step: string;
// Flow data (optional until set)
serviceId?: string;
slotDate?: string;
slotTime?: string;
// Simple counters
bookingsCount?: number;
}
すべてのセッションは initial() から開始します。
initial: () => ({
step: "idle",
bookingsCount: 0,
})
ハーネスの分離
テストハーネスは makeBot() を使って、仕様ごとに新しいボットを作成します。各ボットはそれぞれ独自の MemorySessionStorage を使用します。
Spec 1 ("booking flow"):
makeBot() → fresh MemorySessionStorage → session starts from initial()
Spec 2 ("cancel flow"):
makeBot() → another fresh MemorySessionStorage → session starts from initial()
- 仕様間でセッションが漏れることはありません
- 実行間のクリーンアップは不要です
- 各仕様には
initial()で定義された状態だけが渡されます
本番環境での SQLite
本番環境のボットでは、SQLite に切り替えられます(同じ StorageAdapter インターフェースを使うため、ボットのコードも同じです)。
import { SqliteSessionStorage } from "../src/toolkit/storage/sqlite.js"; // planned
const bot = createBot<Session>(token, {
initial: () => ({ step: "idle" }),
storage: new SqliteSessionStorage("./data/sessions.db"),
});
ツールキットに SQLite アダプターが追加されるまでは、grammY の @grammyjs/storage-sqlite を直接使用してください。インターフェースは同じです。
移行
セッションへのフィールドの追加:
// V1
interface Session { step: string; }
// V2 — add optional field (safe, no migration needed)
interface Session {
step: string;
theme?: "light" | "dark"; // optional — defaults to undefined
}
// Access with default:
const theme = ctx.session.theme ?? "light";
- MemoryStorage: 再起動ですべて消去されるため、移行は不要です
- SQLite: オプションのフィールドは安全に追加できます。新しい必須フィールドを追加する場合は、既存の行にデフォルト値を設定する移行手順が必要です
クイックリファレンス
| 項目 | grammY | ツールキット |
|---|---|---|
| セッションを有効化 | bot.use(session({...})) | createBot() によって自動設定 |
| ctx.session の型指定 | Context & SessionFlavor<S> | BotContext<S>(事前定義済み) |
| ストレージ(開発) | インメモリ Map(デフォルト) | MemorySessionStorage(デフォルト) |
| ストレージ(本番環境) | @grammyjs/storage-sqlite | SqliteSessionStorage(予定) |
| セッションキー | chatId_userId | 同じ |
| ハーネスの分離 | 手動設定 | 自動 — 仕様ごとに新規作成 |
よくある間違い
- シリアライズできないデータを保存する — 関数やクラスのインスタンスは保存できません。プレーンオブジェクトのみを使用してください。
initial()でフィールドを初期化しない — フィールドがない場合、デフォルト値ではなくundefinedになります。- 再起動後もセッションが維持されることを前提にする(MemoryStorage) — セッションは一時的です。再起動に耐えられるようにフローを設計してください。
- セッションはユーザー単位ではなくチャット単位です — 同じユーザーでも、チャットが異なればセッションも異なります。
- セッションキーは
chatId_userIdという文字列です —chat.idだけと混同しないでください。