はじめに
こんにちは!今回は、LINEで受信したメッセージを自動的にZoomチャンネルに転送するシステムを作ってみました。
企業でのSlack連携はよく見かけますが、ZoomのTeam Chat機能を使った連携事例はまだ少ないのではないでしょうか?そこで実際に動くシステムを構築してみたので、その実装方法をご紹介します!
作ったもの
- LINEでメッセージを送ると、Zoomの指定チャンネルに自動転送
- Zoomでスレッド返信すると、元のLINEユーザーに自動返信
- 非同期処理によるレスポンシブな応答
- モジュール化された保守しやすいアーキテクチャ
- メッセージID管理による双方向突合機能
システム構成
📱 LINE公式アカウント
↓ Webhook (メッセージ送信)
🔧 Node.js Express Server
↓ Zoom API (メッセージ投稿)
💬 Zoom Team Chat
↓ Webhook (返信通知)
🔧 Node.js Express Server
↓ LINE Push API (返信転送)
📱 LINE公式アカウント
- LINE → Zoom への転送だけでなく
- Zoom → LINE への返信転送も実現
ディレクトリ構成
📁 zoom-line-bridge/
├── 📄 app.js # メインエントリーポイント
├── 📁 routes/
│ └── 📄 webhook.js # LINE Webhook処理
├── 📁 services/
│ ├── 📄 messageProcessor.js # メイン処理ロジック
│ ├── 📄 lineApi.js # LINE API通信
│ ├── 📄 externalApi.js # Zoom API通信
│ └── 📄 messageMappingService.js # メッセージID管理
├── 📁 utils/
│ └── 📄 formatter.js # メッセージ整形
└── 📄 .env # 環境変数
技術的なポイント
1. Reply API + Push API のハイブリッド構成
LINEのメッセージ応答では、Reply API(無料・即座)とPush API(有料・非同期)を使い分けることで、ユーザー体験とコストを最適化できます。
// 即座に応答(Reply API)
await lineApi.replyMessage(replyToken, 'メッセージありがとうございます!返信まで今しばらくお待ちください。');
// 長時間処理後に結果送信(Push API)
await lineApi.pushMessage(userId, resultMessage);
2. Zoom OAuth認証の自動化
Zoom APIでは今回、Server-to-Server OAuthを使用しています。ユーザー認証なしで自動的にアクセストークンを取得できます。(条件あり)
3. 双方向通信のためのWebhook連携
さらにZoomのWebhook機能を活用してチャット返信イベントを検知し、LINEに自動転送する機能が実装できます。
// Zoomからの返信を検知
if (event === 'chat_message.replied') { //chat_message.replied イベント利用
const message = payload.object.message;
const parentMsgId = payload.object.parent_msg_id;
// メッセージIDマッピングから元のLINEユーザーを特定
const mappedData = messageMap.get(parentMsgId);
if (mappedData) {
// LINEにPush APIで返信転送
await lineApi.pushMessage(mappedData.userId, message);
}
}
4. MessageMappingによる突合管理
ZoomメッセージIDとLINEユーザーIDのマッピングを管理することで、双方向通信や確実な個別返信を実現できます。この辺りは実装方法も人それぞれになるかなと思います。あえて詳細を記載しませんが、信頼性や実装の複雑性、実装フェーズなどに応じてマッピング機能を実装します。
セットアップ手順
1. Zoom App作成
- Zoom Marketplace でDeveloper Accountを作成
- 「Build App」→「Server-to-Server OAuth」を選択
- 必要なスコープを設定:
-
chat_channel:read
(チャンネル一覧取得) -
chat_message:write
(メッセージ送信)
-
- Client ID、Client Secret、Account IDを取得
双方向通信のためのWebhook設定:
5. 「Event Subscriptions」を有効化
6. Webhook URLを設定(https://your-domain.com/zoom-webhook
)
7. 「Chat Message Events」でchat_message.replied
を選択
8. Webhook Tokenを取得
2. LINE Developers設定
- LINE Developers Console でChannelを作成
- Webhook URLを設定(
https://your-domain.com/webhook
) - Channel Access Tokenを取得
LINE Developerによると複数の種類のChannel Access Tokenがあるようです。セキュリティポリシーに応じて検討してください。v2.1でも問題なく投稿できました。
実装
1. 環境変数設定
# .env
# LINE Bot設定
CHANNEL_ACCESS_TOKEN=your_line_channel_access_token
# Zoom API設定
ZOOM_CLIENT_ID=your_zoom_client_id
ZOOM_CLIENT_SECRET=your_zoom_client_secret
ZOOM_ACCOUNT_ID=your_zoom_account_id
ZOOM_WEBHOOK_TOKEN=your_zoom_webhook_token
2. LINE Webhook受信
// routes/webhook.js
router.post('/webhook', async (req, res) => {
try {
const event = req.body.events[0];
const message = event.message.text;
const userId = event.source.userId;
const replyToken = event.replyToken;
// すぐに200 OKを返す
res.status(200).json({ message: 'OK' });
// 非同期処理開始
messageProcessor.processMessage(userId, message, replyToken);
} catch (error) {
console.error(error);
res.status(500).json({ error: 'Internal Server Error' });
}
});
3. Zoom API連携
Zoom Team Chat への送信には、 /v2/chat/users/{userId}/messages
エンドポイントを利用します。userIdに特定のユーザーを入れることで、そのユーザーからの発信という形で投稿が可能です。以下の例では、PoC的に /me/
を入れています。これは、この Server-to-Server OAuth アプリを作ったユーザーになります。
// services/externalApi.js
async sendToZoomChannel(inputData) {
const messageText = `新着メッセージ:${inputData}`;
const response = await axios.post('https://api.zoom.us/v2/chat/users/me/messages', {
to_channel: generalChannel.id,
message: messageText
}, {
headers: {
Authorization: `Bearer ${this.zoom.accessToken}`,
'Content-Type': 'application/json'
}
});
return {
channelId: generalChannel.id,
channelName: generalChannel.name,
messageId: response.data.id
};
}
}
このメソッドを呼び出し、投稿に成功するとレスポンスとして以下のような message_id
が取得できます。
{
"id": "8cfaf567-bf5a-4acc-b4f2-88b3d371aca5"
}
このIDは後ほどメッセージの返信マッピングを行うにあたって使うので、別途保存しておきます。
4. Zoom Webhook受信
先ほど掲載したものの再掲になりますが、chat_message.replied
を受けてpayloadから
- 誰に対する返信か(
parent_msg_id
) - メッセージの内容(
message
)
を取得し parent_msg_id
と、上述のプロセスで取得していたメッセージのIDと突合します。
router.post('/zoom-webhook', async (req, res) => {
try {
const event = req.body.event;
const payload = req.body.payload;
if (event === 'endpoint.url_validation') {/* 省略 */}
if (event === 'chat_message.replied') {
const message = payload.object.message;
const parentMsgId = payload.object.parent_msg_id;
// parent_msg_idが一致する場合、LINEにメッセージを送信
const mappedData = messageMap.get(parentMsgId);
if (mappedData) {
const replyMessage = message;
try {
await lineApi.pushMessage(mappedData.userId, replyMessage);
} catch (error) {
console.error(error);
}
} else {
//マッピングがなかった場合の処理
}
}
res.status(200).json({ message: 'OK' });
} catch (error) {
console.error(error);
res.status(500).json({ error: 'Internal Server Error' });
}
});
5. デプロイ
ざっくりですが、それぞれの設定が完了後、冒頭の環境変数を準備します。
# 依存関係インストール
npm install express axios dotenv
# 環境変数設定
nano .env
# .envファイルを編集
# サーバー起動
npm start
応用アイデア
- 画像・ファイルの転送対応
- メンション機能: @指定でZoom内の特定ユーザーに通知
- 自動翻訳機能: 多言語チーム対応
- チャンネル振り分け: メッセージ内容に応じて送信先変更
要検討事項
今回の実装はあくまでPoCなので、実際に活用していくにあたっては権限管理や、監査、ロギング、またAPIなどのエラーハンドリングやAPIのRate Limitへのケアなどが必要です。状況に応じてご検討ください。
まとめ
顧客サポート、社内ヘルプデスク、チーム間連携など、様々な用途で活用できるシステムです。企業のコミュニケーションツール統合に興味がある方は、ぜひ試してみてください!
参考リンク
LINE Developersに加えて、こちらの資料は大変参考になりました。有名かもしれませんが、リンク貼っておきます。