Cursor に Webhook ドキュメントを読ませたら、30分で Telegram→Slack メッセージリレーが完成した — UnifyPort
Telegram のメッセージを Slack に転送したい場合、通常のルートは @BotFather でボットを作成し、Webhook か getUpdates のポーリングを設定し、Telegram 固有の Update オブジェクトを解析し、Slack の Incoming Webhook 用にフォーマットし、レートリミットに対処する、というものです。動作はしますが、コードは完全に Telegram 専用です。Update オブジェクトは独自の構造を持ち、ボットは自身の会話しか見えません。後から同じ Slack チャンネルで WhatsApp メッセージも受信したくなったら、まったく別の API をゼロから繋ぎ込む必要があります。
私は別のルートを選びました。Cursor を開き、UnifyPort の Webhook リファレンスを貼り付け、Telegram→Slack メッセージリレーの構築を依頼しました。30分後には、すべての配信を HMAC-SHA256 で署名検証し、Slack Block Kit でフォーマットしてチャンネルに投稿する Express サーバーが完成 — 約45行のコードです。同じリレーが WhatsApp、LINE、TikTok、Zalo、X のメッセージもコード変更なしで処理できます。6つのプラットフォームすべてで受信イベントのデータ構造が同一だからです。
完成するもの
以下を実現する Node.js Express サーバー:
- UnifyPort 統一 Webhook が送信する
message.receivedイベントを受信 signing_secretを使用した HMAC-SHA256 で各配信の署名を検証- 各メッセージをプロバイダータグ・送信者・タイムスタンプ付きの Slack Block Kit メッセージにフォーマット
- Incoming Webhook 経由で Slack チャンネルに投稿
所要時間:約30分。UnifyPort ワークスペース(QR コードスキャンで Telegram アカウントを接続済み — ボット登録不要)と Slack の Incoming Webhook URL が必要です。
なぜ Bot API を直接使わないのか?
Telegram Bot API のルートで必要な作業を見てみましょう:
- @BotFather でボットを作成 — bot token を取得しますが、ボットは個人アカウントとは別のエンティティ
- メッセージ受信方法を選択:
getUpdatesポーリングまたは Webhook — どちらも Telegram 固有のセットアップが必要 Updateオブジェクトを解析:メッセージ本文はupdate.message.text、送信者はupdate.message.from.id、チャットはupdate.message.chat.id— Telegram 固有のデータ構造- レートリミットへの対処:グローバルで30メッセージ/秒、同一グループで20メッセージ/分
- フォーマットして Slack に転送
コードは動きます — 同じチャンネルで WhatsApp メッセージも必要になるまでは。そうなると WhatsApp Cloud API を別途統合し、異なる Webhook ペイロードを解析し、2つのコードパスを維持して、どちらも Slack メッセージを出力することになります。3つのプラットフォームなら3つのコードパスです。
UnifyPort の Webhook はこれを一本化します。すべてのプラットフォームが同じ message.received イベントを配信 — provider、from、text、message_id。ひとつのハンドラーで6チャンネルすべてに対応できます。
準備:ドキュメントを Cursor に読ませる
プロンプトを書く前に、API リファレンスを Cursor のコンテキストに入れます。@Docs で UnifyPort Webhook リファレンスを追加してください。特に重要な部分:
message.receivedイベントのペイロード構造x-unifyport-signatureヘッダーと HMAC-SHA256 検証の説明POST /v1/webhook-endpointsエンドポイント作成の API
または、これらのセクションをチャットに直接貼り付けます。重要なのは:正しいフィールド名が入れば、正しいコードが出てくるということです。リファレンスがなければ、Cursor は telegram.onMessage() コールバックや bot.on('text') ハンドラーを発明しますが、これらは UnifyPort の API には存在しません。
Claude Code、Windsurf、Copilot も同様です — ファイルコンテキスト、ドキュメントパネル、またはチャットへの貼り付け。ツールは二次的なもの、ドキュメントが本質です。
ビルド
最初のプロンプト — リレーの骨格:
UnifyPort の webhook ドキュメントを使って、POST /webhook ルートを持つ Express サーバーを書いてください。event が “message.received” のとき、SLACK_WEBHOOK_URL 環境変数で指定された Slack incoming webhook にフォーマット済みメッセージを送信します。provider 名、送信者、メッセージ本文を含めてください。すべてのイベントに 200 を返します。
Cursor がリファレンスを読んで生成:
import express from "express";
const SLACK_WEBHOOK = process.env.SLACK_WEBHOOK_URL;
const app = express();
app.use(express.json());
app.post("/webhook", async (req, res) => {
const evt = req.body;
if (evt.event === "message.received") {
await fetch(SLACK_WEBHOOK, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
text: `*[${evt.provider}]* ${evt.from}: ${evt.text}`,
}),
});
}
res.sendStatus(200);
});
app.listen(3000, () => console.log("listening on :3000"));
ハンドラーは15行。イベント構造が6つのプラットフォームすべてで同一なので、Telegram 固有の解析は不要です。evt.provider の値は "telegram" ですが、構造は WhatsApp や LINE のメッセージとまったく同じです。
2つ目のプロンプト — 署名検証:
各配信には x-unifyport-signature ヘッダーが含まれます。環境変数の signing_secret を使って HMAC-SHA256 で検証してください。HMAC は生のリクエストボディに対して計算する必要があります — 再シリアライズした JSON ではありません。検証失敗時は 401 を返します。タイミングセーフな比較を使ってください。
Cursor が検証レイヤーを追加:
import crypto from "crypto";
const SIGNING_SECRET = process.env.UNIFYPORT_SIGNING_SECRET;
app.use(express.json({
verify: (req, _res, buf) => { req.rawBody = buf; }
}));
function isValid(req) {
const sig = req.get("x-unifyport-signature") || "";
const expected = crypto
.createHmac("sha256", SIGNING_SECRET)
.update(req.rawBody)
.digest("hex");
return sig.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
}
verify コールバックが Express の JSON パース前に生のバッファーをキャプチャします。req.body を再シリアライズしていたら、空白文字の違いで HMAC が一致しません。プロンプトで「生のリクエストボディ」と指定したため、修正なしで正しく実装されました。
3つ目のプロンプト — リッチな Slack フォーマット:
Slack メッセージを Block Kit 形式に変更してください。provider をタグとして、送信者 ID を太字で表示します。timestamp と message_id を含む context ブロックを追加してください。
完成した server.js:
import express from "express";
import crypto from "crypto";
const SIGNING_SECRET = process.env.UNIFYPORT_SIGNING_SECRET;
const SLACK_WEBHOOK = process.env.SLACK_WEBHOOK_URL;
const app = express();
app.use(express.json({
verify: (req, _res, buf) => { req.rawBody = buf; }
}));
function isValid(req) {
const sig = req.get("x-unifyport-signature") || "";
const expected = crypto
.createHmac("sha256", SIGNING_SECRET)
.update(req.rawBody)
.digest("hex");
return sig.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
}
app.post("/webhook", async (req, res) => {
if (!isValid(req)) return res.sendStatus(401);
const evt = req.body;
if (evt.event === "message.received" && SLACK_WEBHOOK) {
await fetch(SLACK_WEBHOOK, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
blocks: [
{
type: "section",
text: {
type: "mrkdwn",
text: `*[${evt.provider}]* from *${evt.from}*\n${evt.text}`,
},
},
{
type: "context",
elements: [
{
type: "mrkdwn",
text: `${evt.message_id} · ${new Date(evt.timestamp * 1000).toISOString()}`,
},
],
},
],
}),
});
}
res.sendStatus(200);
});
app.listen(3000, () => console.log("listening on :3000"));
50行未満。署名検証、Slack Block Kit フォーマット、全受信メッセージの転送。ハンドラーは provider の値で分岐しません — その必要がないからです。
起動して Telegram メッセージの到着を確認
UnifyPort ワークスペースで Telegram アカウントを接続します — Telegram は QR コード認証に対応しているので、Telegram アプリでスキャンすれば数秒で完了です。@BotFather 不要、bot token 不要、API 認証情報の管理も不要。接続されるのはあなたの実際の Telegram アカウントであり、独立したボットではありません。
サーバーを指す Webhook エンドポイントを登録し、subscribed_events: ["message.received"] と signing_secret を設定します。サーバーを起動し、別のユーザーからあなたの Telegram アカウントにテストメッセージを送信します。イベントが到着:
{
"event": "message.received",
"account_id": "acct_5Qm8nR",
"provider": "telegram",
"from": "user_7c3d9e",
"text": "明日の3時、まだ打ち合わせする?",
"timestamp": 1750521600,
"message_id": "tg_msg_2a6f4b"
}
サーバーが署名を検証し、Block Kit ペイロードをフォーマットして Slack に投稿します。メッセージがチャンネルに表示され、プロバイダータグ、送信者、タイムスタンプ、メッセージ全文が確認できます。署名検証が 401 で失敗する場合は、UNIFYPORT_SIGNING_SECRET が Webhook エンドポイントに設定した値と一致しているか確認してください。
アーキテクチャの恩恵:WhatsApp を追加してもコード変更ゼロ
ここで、このリレーと Bot API ビルドの違いが明確になります。同じワークスペースで WhatsApp アカウントを接続し、同じ Webhook エンドポイントをサブスクライブします。WhatsApp メッセージはまったく同じ構造で到着します:
{
"event": "message.received",
"account_id": "acct_3Xk1wL",
"provider": "whatsapp",
"from": "user_9b4e7a",
"text": "請求書を送りました。受領確認をお願いします",
"timestamp": 1750521660,
"message_id": "wa_msg_8d2c5f"
}
同じハンドラー。同じ Slack チャンネル。Slack では [whatsapp] タグが [telegram] と区別されますが、コードパスは完全に同一です。Telegram Bot API リレーなら、別途統合が必要です — 異なる Webhook ペイロード、異なる解析ロジック、異なるエラー処理。このリレーは LINE、TikTok、Zalo、X も同じ方法で処理します。イベント構造がプラットフォーム間で変わらないからです。
これが、単一プラットフォームの API に対して構築するのと、正規化レイヤーに対して構築するのとの違いです。Bot API のルートはリレーを Telegram に縛り付けます。UnifyPort の Webhook リファレンスは、Cursor(またはどの AI コーディングツールでも)に対してシンプルで完結しやすいインターフェースを提供し、構築したコードは後から接続するすべてのチャンネルでそのまま動きます。お使いのツールに UnifyPort API リファレンス を読ませ、message.received のスキーマを貼り付ければ、ランチ前にはリレーが稼働しているでしょう。