← 全記事
チュートリアル

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 サーバー:

  1. UnifyPort 統一 Webhook が送信する message.received イベントを受信
  2. signing_secret を使用した HMAC-SHA256 で各配信の署名を検証
  3. 各メッセージをプロバイダータグ・送信者・タイムスタンプ付きの Slack Block Kit メッセージにフォーマット
  4. Incoming Webhook 経由で Slack チャンネルに投稿

所要時間:約30分。UnifyPort ワークスペース(QR コードスキャンで Telegram アカウントを接続済み — ボット登録不要)と Slack の Incoming Webhook URL が必要です。

なぜ Bot API を直接使わないのか?

Telegram Bot API のルートで必要な作業を見てみましょう:

  1. @BotFather でボットを作成 — bot token を取得しますが、ボットは個人アカウントとは別のエンティティ
  2. メッセージ受信方法を選択getUpdates ポーリングまたは Webhook — どちらも Telegram 固有のセットアップが必要
  3. Update オブジェクトを解析:メッセージ本文は update.message.text、送信者は update.message.from.id、チャットは update.message.chat.id — Telegram 固有のデータ構造
  4. レートリミットへの対処:グローバルで30メッセージ/秒、同一グループで20メッセージ/分
  5. フォーマットして Slack に転送

コードは動きます — 同じチャンネルで WhatsApp メッセージも必要になるまでは。そうなると WhatsApp Cloud API を別途統合し、異なる Webhook ペイロードを解析し、2つのコードパスを維持して、どちらも Slack メッセージを出力することになります。3つのプラットフォームなら3つのコードパスです。

UnifyPort の Webhook はこれを一本化します。すべてのプラットフォームが同じ message.received イベントを配信 — providerfromtextmessage_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 のスキーマを貼り付ければ、ランチ前にはリレーが稼働しているでしょう。