mcpField
T
telegram-bot-ui

telegram-bot-ui

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

telegram-bot-ui について

telegram-bot-uiは、Telegramボットのキーボード構築と操作処理を扱うスキルです。ボタン、コマンド、ForceReplyの使い分けを説明し、メニュー、確認、ページ送り用のビルダーを紹介します。

主な機能

  1. 01インライン・返信キーボードの解説
  2. 02ボタンとテキスト入力の使い分け
  3. 03メニューと確認キーボードの構築
  4. 04ページ送り用キーボードの作成
  5. 05URL・コピー・Web Appボタンに対応
  6. 06grammYのコールバック処理を解説

活用シーン

  1. 01ボタン式メインメニューの追加
  2. 02ForceReplyによる自由入力の収集
  3. 03確認・ページ送りフローの構築
  4. 04コピー可能なテキストやMini Appボタンの追加

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/agntdev/skills --skill telegram-bot-ui

インストール後、Claude Code で/telegram-bot-uiを入力して読み込まれたか確認します。

同梱ファイルreferences/grammY-keyboards.md

詳細

telegram-bot-ui Skill

Telegram キーボードの接続方法――仕組みだけを扱います。ボットが何を話し、どんな雰囲気にするかについては、 telegram-bot-ux をご覧ください。

⭐ ボタン優先 — 機能にアクセスするための基本方針

これらのボットを開発・提供するオーナーは技術者ではなく、ユーザーも同様です。ユーザーはスラッシュコマンドを覚えるのではなく、ボタンをタップしてボットを操作します。そのため、機能へアクセスできるようにするには、bot.command(...) を新たに作るのではなく、/start のメインメニューにボタンを追加します。

bot-starter テンプレートでは、これは 1 行でできます。src/handlers/<slug>.ts の先頭で registerMainMenuItem({ label, data }) を呼び出し、.callbackQuery(...) でその data を処理します。提供されている /start が、項目を集約したメニューを表示します。ボットが公開するスラッシュコマンドは、/start、/help、そして必要に応じて下記の自由入力だけにしてください。機能ごとにスラッシュコマンドを追加してはいけません。そうすると、人が使いこなせない、20 個以上の難解で重複したコマンドを抱えるボットになってしまいます。

これは「コマンドを一切使わない」という意味ではありません。ボタンとコマンドの使い分けのヒューリスティックをご覧ください。検索クエリ、メモ、住所、日付、時刻、金額など、ボットが予測できない自由入力には、引き続きコマンドと ForceReply が適しています。ナビゲーションと選択にはボタン、ボットが予測できない入力にはコマンドを使います。

agntdev パイプライン向けに作られています。 詳しくは、 agnt-cli-builder の 発見とタスク獲得のループをご覧ください。このスキルでは、 獲得したタスクの実装で使うインラインボタン、メニュー、 ページネーション、confirmKeyboard、ForceReply、copy_text のパターンを説明します。


1. Telegram キーボードの仕組み(Bot API)

Telegram には2 種類のキーボードと、特殊な入力向けの複数のマークアップ形式があります。用途によって JSON の形も異なります。

InlineKeyboardMarkup

特定のメッセージに付けるボタンです。タップすると、チャットにはメッセージを送らずに、callback_query がボットに送られます。メニュー、確認、ページネーションに適しています。

// Attached to sendMessage reply_markup field
{
  "inline_keyboard": [
    [
      { "text": "Yes", "callback_data": "confirm:42:yes" },
      { "text": "No",  "callback_data": "confirm:42:no" }
    ],
    [
      { "text": "Open Site", "url": "https://example.com" }
    ]
  ]
}

「Yes」をタップすると、ボットは data: "confirm:42:yes" を含む callback_query を受け取ります。

制限事項(telegram-bot-api-fundamentals §4 を参照):

  • callback_data は 64 バイト以下(UTF-8)。キリル文字や絵文字は複数バイトです。
  • 1 つのキーボードにつきボタンは 100 個以下です。
  • 行数は 8 行以下です(iOS では約 5 行でスクロールします)。
  • ボタンのテキストは約 64 文字以下です(超えると切り詰められます。モバイルでは 24 文字以下にしてください)。
ReplyKeyboardMarkup

ユーザーのキーボードを置き換える、常時表示されるボタンです。タップすると通常のテキストメッセージが送信されます。常設メニューやクイック返信に適しています。

{
  "keyboard": [
    [{ "text": "📅 Book" }, { "text": "📋 My bookings" }],
    [{ "text": "❌ Cancel" }]
  ],
  "resize_keyboard": true,
  "one_time_keyboard": false,
  "input_field_placeholder": "Type or tap…",
  "selective": false
}

便利なパラメーター:

  • resize_keyboard — ボタンを小さくします。Telegram Desktop 5.3.2 以降では無視されます(常に 1 ボタンあたり 54px のフラットグリッドを使用し、横幅は 530px が上限です)。
  • one_time_keyboard: true — 最初のタップ後にキーボードを非表示にします。一度だけ表示するプロンプトに適しています。
  • input_field_placeholder — キーボードの上にある入力欄に表示するテキストです。ユーザーにヒントが表示されるため、使ってください。
  • selective: true — グループ内の特定ユーザーにだけ、このキーボードを表示します(まれなケースです)。
ForceReply

チャット内で返信するようユーザーに促します(インラインの「返信先」UI が表示されます)。ボタンでは受け取れない、必須のテキスト入力に使用してください。

{
  "force_reply": true,
  "input_field_placeholder": "Send your booking address…",
  "selective": false
}

ユーザーには入力欄の上に「Replying to your bot…」というラベルが表示されます。ボット側では、次のテキストメッセージを明示的に処理する必要があります(セッションのステップでフィルタリングします。詳しくは telegram-bot-ux §6 のリニアウィザードをご覧ください)。

ボタンとコマンド — 判断の目安
ユーザーが行いたいこと使用するもの
少数の固定された選択肢(はい/いいえ、選択肢 A/B/C、ページ N)から選ぶインラインボタン
メニューを開く、操作を確認する、ページを移動するインラインボタン
自由形式のテキストを送信する(コントラクトアドレス、メモ、検索クエリ)/command +テキスト入力
ボットが解析する構造化引数(日時、時刻、金額)を入力する/command +テキスト入力
必須のテキスト返信(ボットの質問に回答する)ForceReply
ユーザーが繰り返しタップしたい固定ショートカット返信キーボード(控えめに使用)

「インラインボタンをデフォルトにする」という以前のルールは誤りです。選択肢にはボタンが適しており、自由入力にはコマンドが適しています。あらゆる入力をボタンに変えるボットは、パワーユーザーにとって使いにくいものです。メニューにコマンドを使うボットは、モバイルユーザーにとって使いにくいものです。

迷ったときは、こう考えてください。「ユーザーは何を入力すればよいか分かっていますか?」分かっているなら、コマンドを使います。分からない場合(または有効な選択肢が多すぎて覚えられない場合)は、ボタンを使います。

ボタンに表示する文言(動詞を先頭に置く、文頭のみ大文字にする、絵文字の使用量)については、telegram-bot-ux §1 マイクロコピーを参照してください。

編集と送信
# Edit existing message (for inline keyboards — user doesn't see new messages)
POST /bot<TOKEN>/editMessageText   { chat_id, message_id, text, reply_markup }

# Edit only the keyboard (keep text)
POST /bot<TOKEN>/editMessageReplyMarkup  { chat_id, message_id, reply_markup }

# Send new message (for reply keyboard flows)
POST /bot<TOKEN>/sendMessage  { chat_id, text, reply_markup }

2. grammY — キーボードの使用

キーボードの添付、コールバックの処理、メッセージの編集と新規送信の UX パターンに関する grammY の仕組みは、references/grammY-keyboards.md に記載されています。インライン/返信/ForceReply/連絡先リクエスト/位置情報リクエスト/ユーザーリクエスト/チャットリクエスト/管理対象ボットリクエストの正確な reply_markup の形式、コールバックデータのプレフィックスによるルーティング規則、Telegram のメッセージ内編集に関する UX の注意点が必要な場合は、このファイルを読んでください。


3. ツールキット(src/toolkit/)— UI ビルダー

インライン化されたツールキット(bot-starter テンプレートの src/toolkit/)は、プレーンな InlineKeyboardMarkup オブジェクトを返す純粋なビルダーを提供します。grammY のインポートは不要です。grammY が期待する JSON 形式をそのまま生成します。

import {
  inlineButton, urlButton, copyTextButton, webAppButton,
  inlineKeyboard, menuKeyboard, confirmKeyboard, paginate,
} from "../src/toolkit/ui/buttons.js";
inlineButton(text, callbackData)
inlineButton("Yes", "confirm:42")
// → { text: "Yes", callback_data: "confirm:42" }
// Type: { text: string; callback_data: string }
urlButton(text, url)
urlButton("Docs", "https://agnt-gm.ai")
// → { text: "Docs", url: "https://agnt-gm.ai" }
// Type: { text: string; url: string }
copyTextButton(text, copyText) — ワンタップでコピー
copyTextButton("📋 Copy order ID", "ORD-12345")
// → { text: "📋 Copy order ID", copy_text: { text: "ORD-12345" } }
// Type: { text: string; copy_text: { text: string } }

ID、アドレス、コード、リンクなど、ユーザーが通常は長押ししてコピーするものに使用します。操作ごとにタップを 4 回節約できます。telegram-bot-ux §10 のアンチパターン「ID に copy_text を使わない」も参照してください。

webAppButton(text, url) — ミニアプリを開く
webAppButton("🛒 Open shop", "https://shop.example.com/twa")
// → { text: "🛒 Open shop", web_app: { url: "https://shop.example.com/twa" } }
// Type: { text: string; web_app: { url: string } }

Bot API 9.4 以降に対応しています。フローがインラインキーボードで対応できる範囲を超えた場合に使用します。明確な 4 つの判断基準については、telegram-bot-ux §8 ミニアプリへの移行を参照してください。

inlineKeyboard(rows)

ボタンの行を有効な InlineKeyboardMarkup にまとめます。

const kb = inlineKeyboard([
  [inlineButton("A", "a"), inlineButton("B", "b")],
  [urlButton("Docs", "https://x.io")],
]);

await ctx.reply("Choose:", { reply_markup: kb });
menuKeyboard(items, columns?)

フラットなリストからグリッドレイアウトを作成します。デフォルトは 1 列です。

const items = [
  { text: "📅 Book", data: "menu:book" },
  { text: "📋 My bookings", data: "menu:my" },
  { text: "❌ Cancel", data: "menu:cancel" },
  { text: "ℹ️ Help", data: "menu:help" },
];

menuKeyboard(items);     // vertical list, 1 per row
menuKeyboard(items, 2);  // 2-column grid
confirmKeyboard(actionPrefix, opts?)

はい/いいえの行です。コールバックは <prefix>:yes / <prefix>:no です。

confirmKeyboard("delete:42");
// → [✅ Yes ("delete:42:yes")] [❌ No ("delete:42:no")]

confirmKeyboard("publish", { yes: "🚀 Publish", no: "🔙 Back" });

ハンドラー:

bot.on("callback_query:data", async (ctx) => {
  const data = ctx.callbackQuery.data;
  if (data.startsWith("delete:")) {
    const [, id, action] = data.split(":");
    await ctx.editMessageText(action === "yes" ? `Deleted ${id}` : "Cancelled");
  }
  await ctx.answerCallbackQuery();
});
paginate(items, options)

項目をページに分割し、前へ/次へコントロールを付けます。

const result = paginate(allItems, {
  page: 0,               // 0-based, auto-clamped
  perPage: 5,
  callbackPrefix: "pg",   // default: "page"
  prevLabel: "« Prev",
  nextLabel: "Next »",
});

// result.pageItems  — items for current page
// result.totalPages — how many pages
// result.page       — actual page number (may be clamped)
// result.controls   — InlineKeyboardMarkup with prev/next buttons

1 ページの場合 → コントロールなし。最初のページの場合 → 「次へ」のみ。最後のページの場合 → 「前へ」のみ。中間のページの場合 → 両方。

ページネーションの完全なハンドラー:

async function showPage(ctx: BotContext, page: number) {
  const items = await loadItems();
  const { pageItems, controls } = paginate(items, { page, perPage: 5 });

  const rows = pageItems.map(item => [
    inlineButton(item.name, `select:${item.id}`),
  ]);

  const keyboard = inlineKeyboard([...rows, ...controls.inline_keyboard]);
  await ctx.editMessageText("Choose an item:", { reply_markup: keyboard });
}

bot.on("callback_query:data", async (ctx) => {
  const data = ctx.callbackQuery.data;
  if (data.startsWith("pg:next:")) await showPage(ctx, parseInt(data.split(":")[2]));
  if (data.startsWith("pg:prev:")) await showPage(ctx, parseInt(data.split(":")[2]));
  await ctx.answerCallbackQuery();
});

コールバック形式: <prefix>:prev:<n> / <prefix>:next:<n>。n は遷移先のページインデックスです。


4. Toolkit と Pure grammY の比較

タスクPure grammYToolkit
インラインボタン{ text: "Hi", callback_data: "x" }inlineButton("Hi", "x")
URL ボタン{ text: "Link", url: "..." }urlButton("Link", "...")
テキストコピーボタン{ text: "Copy", copy_text: { text: "..." } }copyTextButton("Copy", "...")
Web App ボタン{ text: "Open", web_app: { url: "..." } }webAppButton("Open", "...")
グリッドメニュー手動で行に分割menuKeyboard(items, cols)
確認用の行2 ボタンの行を手動で作成confirmKeyboard("prefix")
ページネーション手動でのスライスと前後の処理paginate(items, opts)

Toolkit のビルダーは、grammY が期待するものと同じ JSON の形を生成します。あくまで利便性のためのもので、依存を強制するものではありません。定型コードを減らしたいときに使ってください。完全な制御が必要な場合は使わなくてもかまいません。


よくある間違い

  1. keyboard フィールドに inline_keyboard を指定する — これらは別のオブジェクトです。インラインキーボードは reply_markup.inline_keyboard の下に、返信キーボードは reply_markup.keyboard の下に指定します。
  2. ハンドラーなしで paginate() を使う — ボタンは pg:prev:X/pg:next:X のデータを生成します。コールバックハンドラーでこれらを振り分ける必要があります。
  3. 未知のコールバックを処理しない — 想定外のコールバックデータに備えて、常にフォールバックの answerCallbackQuery を用意してください。
  4. 新しいメッセージに editMessageText を使う — 以前の返信から message_id を取得する必要があります。最初のメッセージには ctx.reply() を、更新には ctx.editMessageText() を使います。
  5. 非 ASCII 文字を含む 64 バイトの callback_data — Telegram は BUTTON_DATA_INVALID で拒否します。上限は文字数ではなくバイト数です。telegram-bot-api-fundamentals §4 を参照してください。
  6. ステップフィルターなしで ForceReply を使う — チャット内のすべてのテキストメッセージが「X を待機中」のハンドラーをトリガーします。ctx.session.step で条件を指定してください。telegram-bot-ux §6 を参照してください。
  7. 返信キーボードで resize_keyboard: true を指定し、デスクトップでも有効だと想定する — Telegram Desktop 5.3.2+ では resize_keyboard が無視されます。最大 4 列を前提に設計してください。
  8. input_field_placeholder の指定漏れ — ユーザーには入力欄が空に見え、何を入力すればよいかわかりません。必ず設定してください。
  9. 一度限りのプロンプトで one_time_keyboard: false(デフォルト)を使う — ユーザーがタップした後もキーボードが表示されたままになります。「一度タップすれば完了」というフローでは one_time_keyboard: true を設定してください。
  10. ユーザーがすでに共有した後も RequestContact 用のカスタムキーボードを表示する — 古いキーボードが表示されたままになります。remove_keyboard: true を指定した新しいメッセージを送信するか、キーボードを更新してください。

コメント