開発者ドキュメント
日本語

ボット開発

ボットを作る

@BotFather でボットを作り、メッセージを受け取って返信し、ボタン・コマンド・写真・グループまで — 最初から最後までたどるチュートリアル。

このチュートリアルを終えると、メッセージに返信し、ボタンを付け、コマンドメニューや写真を扱い、グループでも働くボットができあがります。必要なのは Pabal アプリ、ボットプログラムを動かすコンピューター、そして Python 3.10 以上か Node.js 18 以上のどちらかです。

ボットの仕組み

Pabal のボットは、プログラムが操作するアカウントです。ボットに送られたメッセージはボットのメッセージボックスにたまり、ボットプログラムはサーバーに「新着はありますか?」(getUpdates)と尋ねて受け取り、返信を送ります(sendMessage)。ボットプログラムはサーバーの外、あなたのコンピューターで動きます。

人 (アプリ) Pabal サーバー ボットプログラム ① getUpdates?offset=0&timeout=30 新着が来るまで最大 30 秒待つ ② 「やあ」を送る ③ [{update_id: 1, message: "やあ"}] ④ sendMessage(chat_id, "こんにちは!") ⑤ 返信がアプリに届く (リアルタイム) ⑥ getUpdates?offset=2 — 1 は受信済み、次へ
getUpdates で受け取り、sendMessage で返信する基本の流れ

図の説明

  • 3 本の縦線:左はアプリを使う人、中央は Pabal サーバー、右はあなたのボットプログラムです。縦の点線は時間が流れる方向を表します。
  • 灰色の矢印はボットから先に送るリクエスト、青い矢印はその結果としてやり取りされるメッセージです。ボットプログラムはリクエストを送るだけで、サーバーからボットに先に連絡することはありません(Webhook を使わない限り)。
  • ① の待機(ロングポーリング)が要です:timeout=30 を指定すると、新着がないときサーバーはすぐに空の応答を返さず、最大 30 秒保留して、メッセージが届いた瞬間(②)に返します(③)。そのため反応が速く、リクエスト数は少なくなります。
  • ⑥ の offset が「受信済み」の印です:最後に処理した update_id に 1 を足して送ると、それ以下はサーバーから消えます。offset を上げないと、同じアップデートを受け取り続けます。
  • ⑤ は人が送ったメッセージと同じ経路です:ボットの返信はアプリのすべての端末にプッシュされ、会話一覧にも表示されます。

1. @BotFather でボットを作る

ボットは、Pabal アプリ内の @BotFather と会話して作ります。BotFather は Pabal サーバーに組み込まれたボットです。

  1. アプリの検索欄に BotFather と入力し、BotFather を開きます。開始を押すと、コマンドの一覧が届きます。
  2. /newbot を送ります。
  3. ボットの名前を送ります。会話一覧に表示される名前なので、日本語も使えます。
  4. ボットのユーザー名を送ります。英字で始まる英字・数字・アンダースコアの 5〜32 文字で、必ず bot で終わる必要があります。
  5. トークンの入った返信が届けば完了です。トークンをコピーしておいてください。
/newbot
BotFather새 봇을 만듭니다. 봇의 이름을 알려 주세요. (대화 목록에 보이는 이름이에요)
こんにちはボット
BotFather좋아요. 이제 봇의 사용자명을 정해 주세요. …
hello_test_bot
BotFather완료! 새 봇 @hello_test_bot 를 만들었어요. 검색해서 대화를 시작할 수 있어요. 봇 토큰: 100003:AbCdEfGhIjKlMnOpQrStUvWxYz0123456789 토큰은 비밀번호처럼 안전하게 보관하세요. …

BotFather は現在、韓国語で返信します。上のやり取りでは、BotFather がまずボットの名前(会話一覧に表示される名前)を尋ね、次にボットのユーザー名を尋ね、最後にボット @hello_test_bot ができたことを伝えて、ボットトークンを渡しています(トークンはパスワードのように安全に保管するよう添えられています)。

BotFather のコマンド内容
/newbot新しいボットを作る(名前 → ユーザー名 → トークン)
/mybots自分が作ったボットの一覧
/tokenボットトークンを再表示
/revokeトークンの再発行 — 以前のトークンはただちに無効になり、以前のトークンによる接続も切断される
/setcommandsコマンドメニューの設定(コマンド - 説明 を 1 行に 1 つ)
/deletebotボットの削除 — 네, 삭제합니다(「はい、削除します」)と送って確定。ユーザー名は再び使えるよう解放される
/cancel進行中の操作を取り消す

/token @hello_test_bot のようにユーザー名を付けて送ると、「どのボットですか?」(어느 봇인가요?)と聞かれる段階を飛ばせます。

2. トークンの扱い

トークンは <ボット ID>:<シークレット> の形式です。前半の数字がボットのユーザー ID、後半がシークレットです。トークン 1 つでボットを完全に操作できるので、パスワードと同じように扱ってください。

  • コードに直接書かず、環境変数BOT_TOKEN)やシークレットストアに置いてください。公開リポジトリにアップロードしないでください。
  • 漏れた場合は、BotFather に /revoke を送ります。以前のトークンはただちに拒否され(401 Unauthorized)、以前のトークンで MTProto に接続していたボットのセッションも切断されます。
  • トークンはアドレス(URL)の中に含まれるので、ボットプログラムがリクエストのアドレスをログに残さないようにしてください。Pabal サーバーも Bot API のアドレスをログに残しません。

3. 最初のリクエスト — getMe

すべてのリクエストのアドレスは https://pabal.me/bot<トークン>/<メソッド> です。トークンが正しいか、getMe で確かめてみましょう。

export BOT_TOKEN='100003:AbCdEf…'
curl -s "https://pabal.me/bot$BOT_TOKEN/getMe"
# pip install requests
import os
import requests

r = requests.get(f"https://pabal.me/bot{os.environ['BOT_TOKEN']}/getMe", timeout=10)
print(r.json())
// Node.js 18 以上 — fetch が組み込まれている
const res = await fetch(`https://pabal.me/bot${process.env.BOT_TOKEN}/getMe`);
console.log(await res.json());

成功すると、次のように返ってきます。すべてのレスポンスは、okresult(成功時)または error_codedescription(失敗時)を含む JSON です。

{
  "ok": true,
  "result": {
    "id": 100003,
    "is_bot": true,
    "first_name": "こんにちはボット",
    "username": "hello_test_bot",
    "can_join_groups": true,
    "can_read_all_group_messages": true,
    "supports_inline_queries": false,
    "can_connect_to_business": false,
    "has_main_web_app": false
  }
}

トークンが間違っていると、HTTP 401 とともに {"ok": false, "error_code": 401, "description": "Unauthorized"} が返ってきます。

4. メッセージを受け取る — getUpdates

アプリでボットを開いて開始を押すか、何かメッセージを送ってから、新着を取得します。

curl -s "https://pabal.me/bot$BOT_TOKEN/getUpdates?timeout=30"
{
  "ok": true,
  "result": [
    {
      "update_id": 1,
      "message": {
        "message_id": 1,
        "from": { "id": 100001, "is_bot": false, "first_name": "ハナ" },
        "chat": { "id": 100001, "first_name": "ハナ", "type": "private" },
        "date": 1789805661,
        "text": "/start",
        "entities": [ { "type": "bot_command", "offset": 0, "length": 6 } ]
      }
    }
  ]
}
  • update_id:アップデートごとに 1 ずつ増える番号。処理したあと、次のリクエストで offset=update_id+1 を指定すると、それ以下は「受信済み」として消えます。
  • timeout:新着がないときに待つ秒数(0〜50)。0 ならすぐに空のリストを返します。25〜30 をおすすめします。
  • chat.id:返信の送り先。1:1 の会話なら人の ID(正の数)、グループなら負の数です。
  • 受け取れるアップデートは、message(新しいメッセージ)、edited_message(編集されたメッセージ)、callback_query(ボタンの押下)の 3 種類です。
キューはサーバーのメモリ上にあります

取得されていないアップデートは、ボットごとに直近 1,000 件までサーバーのメモリにたまります。サーバーを再起動すると、取得されていないものは消えます(メッセージ自体は会話に残ります)。ボットを長時間止めたままにしないでください。

5. 返信を送る — sendMessage

curl -s "https://pabal.me/bot$BOT_TOKEN/sendMessage" \
  -H 'Content-Type: application/json' \
  -d '{"chat_id": 100001, "text": "こんにちは!"}'
requests.post(f"https://pabal.me/bot{os.environ['BOT_TOKEN']}/sendMessage",
              json={"chat_id": 100001, "text": "こんにちは!"}, timeout=10)
await fetch(`https://pabal.me/bot${process.env.BOT_TOKEN}/sendMessage`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ chat_id: 100001, text: 'こんにちは!' }),
});

パラメーターは、JSON 本文、フォーム(application/x-www-form-urlencoded)、ファイルをアップロードするときの multipart/form-data、アドレスの後ろのクエリ文字列のうち、使いやすいもので送れます。結果は送信したメッセージ(Message)です。

ボットから先に話しかけることはできません

人がボットに一度でもメッセージを送ったあとでなければ、その人に送ることはできません。そうでない場合は 403 Forbidden: bot can't initiate conversation with a user になります。Telegram と同じルールです。

6. オウム返しボットの完成

受信と送信を繰り返せば、ボットになります。ライブラリを使わずに書いたコードの全体です。

# echo.py — pip install requests
# 実行: BOT_TOKEN='100003:…' python3 echo.py
import os
import requests

API = f"https://pabal.me/bot{os.environ['BOT_TOKEN']}"


def call(method, **params):
    r = requests.post(f"{API}/{method}", json=params, timeout=60)
    data = r.json()
    if not data["ok"]:
        raise RuntimeError(f"{method}: {data['description']}")
    return data["result"]


offset = 0
print("ボットが起動しました。止めるには Ctrl+C")
while True:
    for update in call("getUpdates", offset=offset, timeout=30):
        offset = update["update_id"] + 1          # 受信済みの印
        message = update.get("message")
        if message and "text" in message:
            call("sendMessage", chat_id=message["chat"]["id"], text=message["text"])
// echo.mjs — Node.js 18 以上、ライブラリなし
// 実行: BOT_TOKEN='100003:…' node echo.mjs
const API = `https://pabal.me/bot${process.env.BOT_TOKEN}`;

async function call(method, params = {}) {
  const res = await fetch(`${API}/${method}`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(params),
  });
  const data = await res.json();
  if (!data.ok) throw new Error(`${method}: ${data.description}`);
  return data.result;
}

let offset = 0;
console.log('ボットが起動しました。止めるには Ctrl+C');
for (;;) {
  const updates = await call('getUpdates', { offset, timeout: 30 });
  for (const update of updates) {
    offset = update.update_id + 1;              // 受信済みの印
    const message = update.message;
    if (message?.text) {
      await call('sendMessage', { chat_id: message.chat.id, text: message.text });
    }
  }
}

7. ライブラリで作る

Telegram 用のボットライブラリには、サーバーのアドレスを変える設定があります。その設定ひとつで、Pabal でもそのまま動きます。Telegram 用に作ったボットを移すときもこれだけを変え、トークンは Pabal の BotFather から新しく受け取ってください。

ライブラリ変更する設定確認したバージョン
python-telegram-bot.base_url("https://pabal.me/bot"), .base_file_url("https://pabal.me/file/bot")22.8
aiogramAiohttpSession(api=TelegramAPIServer.from_base("https://pabal.me"))3.31
ライブラリなし(HTTP)アドレスの先頭 https://api.telegram.orghttps://pabal.me
# hello_bot.py — pip install python-telegram-bot
# 実行: BOT_TOKEN='100003:…' python3 hello_bot.py
import os

from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update
from telegram.ext import (Application, CallbackQueryHandler, CommandHandler, ContextTypes,
                          MessageHandler, filters)

SERVER = "https://pabal.me"


def buttons():
    return InlineKeyboardMarkup([[InlineKeyboardButton("👍 いいね", callback_data="like"),
                                  InlineKeyboardButton("🔢 数を増やす", callback_data="count")]])


async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
    await update.message.reply_text("こんにちは!ボタンを押してみてください。", reply_markup=buttons())


async def button(update: Update, context: ContextTypes.DEFAULT_TYPE):
    query = update.callback_query
    if query.data == "like":
        await query.answer("ありがとう!")                  # 押した人の画面に一瞬表示される文言
    else:
        n = context.chat_data.get("n", 0) + 1
        context.chat_data["n"] = n
        await query.answer()                             # 先に応答してから
        await query.edit_message_text(f"数: {n}", reply_markup=buttons())   # メッセージを編集する


async def echo(update: Update, context: ContextTypes.DEFAULT_TYPE):
    await update.message.reply_text(f"「{update.message.text}」と言いましたね。")


def main():
    app = (Application.builder().token(os.environ["BOT_TOKEN"])
           .base_url(f"{SERVER}/bot")                   # api.telegram.org の代わりに Pabal
           .base_file_url(f"{SERVER}/file/bot")
           .build())
    app.add_handler(CommandHandler("start", start))
    app.add_handler(CallbackQueryHandler(button))
    app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, echo))
    print("ボットが起動しました。止めるには Ctrl+C")
    app.run_polling()


if __name__ == "__main__":
    main()
# echo_aiogram.py — pip install aiogram
# 実行: BOT_TOKEN='100003:…' python3 echo_aiogram.py
import asyncio
import os

from aiogram import Bot, Dispatcher
from aiogram.client.session.aiohttp import AiohttpSession
from aiogram.client.telegram import TelegramAPIServer
from aiogram.filters import CommandStart

dp = Dispatcher()


@dp.message(CommandStart())
async def start(message):
    await message.answer("こんにちは!何か送ってみてください。")


@dp.message()
async def echo(message):
    if message.text:
        await message.answer(message.text)


async def main():
    session = AiohttpSession(api=TelegramAPIServer.from_base("https://pabal.me"))   # Pabal
    bot = Bot(os.environ["BOT_TOKEN"], session=session)
    print("ボットが起動しました。止めるには Ctrl+C")
    await dp.start_polling(bot)


asyncio.run(main())

8. ボタンとコールバック

メッセージにインラインボタンを付けるには、reply_markupinline_keyboard(行の配列で、各行はボタンの配列)を指定します。ボタンは、押すとボットに知らせる callback_data ボタンと、リンクを開く url ボタンの 2 種類です。

curl -s "https://pabal.me/bot$BOT_TOKEN/sendMessage" -H 'Content-Type: application/json' -d '{
  "chat_id": 100001,
  "text": "選んでください",
  "reply_markup": {
    "inline_keyboard": [
      [ {"text": "👍 いいね", "callback_data": "like"}, {"text": "👎 いまいち", "callback_data": "dislike"} ],
      [ {"text": "Pabal のドキュメントを開く", "url": "https://pabal.me/docs/"} ]
    ]
  }
}'

人が callback_data ボタンを押すと、ボットは callback_query アップデートを受け取ります。

{
  "update_id": 7,
  "callback_query": {
    "id": "5812039457730125441",
    "from": { "id": 100001, "is_bot": false, "first_name": "ハナ" },
    "message": { "message_id": 4, "chat": { "id": 100001, "type": "private", "first_name": "ハナ" }, "text": "選んでください", … },
    "chat_instance": "8413962145072395171",
    "data": "like"
  }
}

ボットは 10 秒以内に answerCallbackQuery で応答する必要があります。アプリはその間、ボタンに時計のマークを表示して待ちます。

# 画面上部に一瞬表示される文言(show_alert: true なら確認ダイアログ)
curl -s "https://pabal.me/bot$BOT_TOKEN/answerCallbackQuery" -H 'Content-Type: application/json' \
  -d '{"callback_query_id": "5812039457730125441", "text": "ありがとう!"}'

# 押されたメッセージのテキストとボタンを変更する
curl -s "https://pabal.me/bot$BOT_TOKEN/editMessageText" -H 'Content-Type: application/json' \
  -d '{"chat_id": 100001, "message_id": 4, "text": "いいねが押されました 👍"}'
  • callback_data は 1〜64 バイトです。ボタンは 1 つのメッセージに最大 100 個です。
  • コールバックに応答しないと、アプリは 10 秒後に待つのをやめます。ボットが停止している場合は、サーバーがすぐに終了させます。
  • 応答は最初の 1 回だけが有効です。ライブラリによっては、メッセージを編集するときに空の応答を先に送ることがあるので、文言のある応答を先に送ってください。
  • 同じテキスト・同じボタンで編集すると 400 Bad Request: message is not modified になります(Telegram と同じ)。

入力欄の下のキーボード

keyboard を指定すると、入力欄の代わりにボタンパネルが表示され、押すとその文字がメッセージとして送信されます。remove_keyboard で隠し、force_reply で返信モードをオンにします。

{
  "chat_id": 100001,
  "text": "どちらですか?",
  "reply_markup": {
    "keyboard": [ [ {"text": "はい"}, {"text": "いいえ"} ], [ {"text": "現在地を送る", "request_location": true} ] ],
    "resize_keyboard": true,
    "one_time_keyboard": true
  }
}

9. コマンドメニュー

チャット画面で / を押すか、メニューボタンを押すと表示される一覧です。コードで設定するか、BotFather の /setcommands で設定します。

curl -s "https://pabal.me/bot$BOT_TOKEN/setMyCommands" -H 'Content-Type: application/json' -d '{
  "commands": [
    {"command": "start", "description": "はじめる"},
    {"command": "help",  "description": "ヘルプ"}
  ]
}'

コマンドは英小文字・数字・アンダースコアの 1〜32 文字、説明は 1〜256 文字、最大 100 個です。language_code を指定すると言語別の一覧を別に保存しますが、現在アプリには、言語コードなしで設定した既定の一覧しか表示されません。人が送った /start のようなコマンドは、メッセージの entitiesbot_command として示されて届きます。

10. 写真のやり取り

送る

ファイルを multipart/form-data でアップロードするか、以前に受け取った写真の file_id を再利用します。写真は 10MB まで、キャプション(caption)は 1,024 文字までです。URL を指定しての送信は、まだサポートしていません。

curl -s "https://pabal.me/bot$BOT_TOKEN/sendPhoto" \
  -F chat_id=100001 -F caption='今日の写真' -F photo=@sunset.jpg
with open("sunset.jpg", "rb") as f:
    requests.post(f"{API}/sendPhoto", data={"chat_id": 100001, "caption": "今日の写真"},
                  files={"photo": f}, timeout=60)

受け取る

人が送った写真は、メッセージの photo(サイズ別の一覧。Pabal では原本 1 つ)として届きます。getFile でパスを取得してダウンロードします。

# 1) file_id → file_path
curl -s "https://pabal.me/bot$BOT_TOKEN/getFile?file_id=AQAAAAAAAAB7…"
# {"ok":true,"result":{"file_id":"AQAA…","file_unique_id":"AQAA…","file_size":48213,"file_path":"photos/AQAA….jpg"}}

# 2) ダウンロード — アドレスに /file/ が入ります
curl -s -o photo.jpg "https://pabal.me/file/bot$BOT_TOKEN/photos/AQAA….jpg"

11. グループで

  • アプリでグループを作るとき、またはグループ情報 → メンバーを追加 で、ボットのユーザー名を検索して追加します。
  • グループに参加したボットは、グループのすべてのメッセージを受け取ります(Telegram の「プライバシーモード オフ」と同じ)。chat.type"group"chat.id は負の数です。
  • その chat.idsendMessage すると、グループに送られます。ボタン・写真・編集も 1:1 とまったく同じです。
  • ボットがグループから外れると、そのグループへの送信は 403 Forbidden: bot is not a member of the group chat になります。

12. Webhook に切り替える

ボットが公開された HTTPS アドレスを持つサーバーで動いているなら、getUpdates で問い合わせる代わりに、Pabal サーバーから新着をそのアドレスへ送らせることができます。

curl -s "https://pabal.me/bot$BOT_TOKEN/setWebhook" -H 'Content-Type: application/json' \
  -d '{"url": "https://bot.example.com/pabal-webhook", "secret_token": "長いランダムな文字列"}'

設定・検証・再試行・レスポンスでの返信については、Webhook のドキュメントにあります。

MTProto で接続するボット(Telethon)

ボットは、アプリと同じ MTProto でも接続できます。人のアカウント用のツール(Telethon など)をすでに使っているなら便利です。同じボットアカウントなので、HTTP と併用してもかまいません。

# pip install telethon==1.42.0   ← 1.42 を使ってください(下の説明を参照)
# サーバーの公開鍵: https://pabal.me/docs/server-key.pem をダウンロードして同じフォルダーに
import asyncio
import os

from telethon import TelegramClient, events
from telethon.crypto import rsa
from telethon.sessions import StringSession

rsa.add_key(open("server-key.pem").read(), old=False)        # Pabal サーバーの公開鍵
client = TelegramClient(StringSession(), api_id=1, api_hash="0" * 32)
client.session.set_dc(2, "122.34.175.215", 8443)


@client.on(events.NewMessage(incoming=True))
async def echo(event):
    await event.reply(event.raw_text)


async def main():
    await client.start(bot_token=os.environ["BOT_TOKEN"])      # auth.importBotAuthorization
    print("ボットが起動しました。止めるには Ctrl+C")
    await client.run_until_disconnected()


asyncio.run(main())
  • Telethon 1.42 を使ってください。Pabal はレイヤー 216 を話しますが、より新しい Telethon はより高いレイヤーでレスポンスを読もうとするため、ログインの段階から失敗します(TypeNotFoundError)。
  • api_idapi_hash は Pabal では検証しないので、どんな値でもかまいません。
  • MTProto ボットには、サーバーが新しいメッセージをリアルタイムでプッシュします(getUpdates や Webhook は不要)。コールバックへの応答では、event.answer("…")event.edit(…) より先に呼んでください。

ルールと上限

項目
メッセージの文字数 / 写真のキャプション4,096 文字 / 1,024 文字
写真のサイズ(sendPhoto でのアップロード)10MB
リクエスト本文のサイズ12MB
getUpdates の timeout · limit0〜50 秒 · 1〜100 件
未取得のアップデートの保持ボットごとに直近 1,000 件、サーバーのメモリ(再起動で消える)
コールバック応答の待ち時間10 秒
callback_data · ボタン数1〜64 バイト · メッセージあたり 100 個
コマンド英小文字・数字・アンダースコアの 1〜32 文字、説明は 1〜256 文字、最大 100 個
ボットから話しかける不可 — 人が先にボットへ送る必要がある

トラブルシューティング

症状原因と対処
401 Unauthorizedトークンが間違っているか、/revoke で変更されています。BotFather に /token を送って確認してください。
404 Not Found: method not foundPabal がまだサポートしていないメソッドです。メソッド一覧を確認してください。
403 Forbidden: bot can't initiate conversation with a userその人がまだボットに話しかけたことがありません。アプリでボットを開いて、開始を押してもらってください。
409 Conflict: can't use getUpdates method while webhook is activeWebhook が設定されています。deleteWebhook を呼ぶか、Webhook で受け取ってください。
ボタンを押しても反応がないボットが停止しているか、answerCallbackQuery を呼んでいません。
同じメッセージを受け取り続けるoffset を上げていません。処理した update_id + 1 を次のリクエストで指定してください。
太字やリンクの書式が効かないparse_mode はまだサポートしていないため、文字どおりに送られます。コマンド・@メンション・URL・#タグ は、自動的にタップできる形で表示されます。
グループでボットが反応しないボットがグループのメンバーではありません。グループ情報 → メンバーを追加 で追加してください。