mcpField
T
telegram-bot-sessions

telegram-bot-sessions

github.com/agntdev開発143 回インストール17 週間前に更新
リポジトリ

telegram-bot-sessions について

Telegram bot 開発者向けに、grammY で会話状態を管理する方法を説明するスキルです。メモリ内セッション、SQLite による永続化、テスト間の状態分離を扱います。

主な機能

  1. 01grammY セッションと ctx.session の説明
  2. 02MemorySessionStorage の解説
  3. 03SQLite ストレージアダプターの説明
  4. 04セッション構造の設計ガイド
  5. 05テスト単位の状態分離の説明

活用シーン

  1. 01複数メッセージにまたがる会話フローの実装
  2. 02開発用・本番用ストレージの選択
  3. 03セッションデータとフィールド変更の設計
  4. 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 を配信するだけです。以下の処理はボット側で行う必要があります。

  1. 誰がどの段階にいるかを記憶する
  2. メッセージ間で途中のデータを保存する
  3. フローが完了したら状態をクリアする

選択肢:データベース。 メッセージごとにユーザーの状態を 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-sqliteSqliteSessionStorage(予定)
セッションキーchatId_userId同じ
ハーネスの分離手動設定自動 — 仕様ごとに新規作成

よくある間違い

  1. シリアライズできないデータを保存する — 関数やクラスのインスタンスは保存できません。プレーンオブジェクトのみを使用してください。
  2. initial() でフィールドを初期化しない — フィールドがない場合、デフォルト値ではなく undefined になります。
  3. 再起動後もセッションが維持されることを前提にする(MemoryStorage) — セッションは一時的です。再起動に耐えられるようにフローを設計してください。
  4. セッションはユーザー単位ではなくチャット単位です — 同じユーザーでも、チャットが異なればセッションも異なります。
  5. セッションキーは chatId_userId という文字列です — chat.id だけと混同しないでください。

コメント