【Zoom Developer】ZoomとLINE公式チャンネルをリアルタイム連携するメッセージ転送システムを作ってみた

# はじめに

こんにちは！今回は、**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**（有料・非同期）を使い分けることで、ユーザー体験とコストを最適化できます。

```javascript
// 即座に応答（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に自動転送する機能が実装できます。

```javascript
// 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作成

1. [Zoom Marketplace](https://marketplace.zoom.us/) でDeveloper Accountを作成
2. 「Build App」→「Server-to-Server OAuth」を選択
3. 必要なスコープを設定：
   - `chat_channel:read`（チャンネル一覧取得）
   - `chat_message:write`（メッセージ送信）
4. 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設定

1. [LINE Developers Console](https://developers.line.biz/) でChannelを作成
2. Webhook URLを設定（`https://your-domain.com/webhook`）
3. Channel Access Tokenを取得

:::note info
LINE Developerによると複数の種類のChannel Access Tokenがあるようです。セキュリティポリシーに応じて検討してください。v2.1でも問題なく投稿できました。
:::
# 実装

## 1. 環境変数設定

```bash
# .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受信

```javascript
// 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 アプリを作ったユーザーになります。

https://developers.zoom.us/docs/api/team-chat/#tag/chat-messages/POST/chat/users/{userId}/messages


```javascript
// 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` が取得できます。
```json
{
  "id": "8cfaf567-bf5a-4acc-b4f2-88b3d371aca5"
}
```
このIDは後ほどメッセージの返信マッピングを行うにあたって使うので、別途保存しておきます。

## 4. Zoom Webhook受信
先ほど掲載したものの再掲になりますが、`chat_message.replied` を受けてpayloadから
- 誰に対する返信か（`parent_msg_id`)
- メッセージの内容（`message`）

を取得し `parent_msg_id` と、上述のプロセスで取得していたメッセージのIDと突合します。

```javascript
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. デプロイ
ざっくりですが、それぞれの設定が完了後、冒頭の環境変数を準備します。

```bash
# 依存関係インストール
npm install express axios dotenv

# 環境変数設定
nano .env
# .envファイルを編集

# サーバー起動
npm start
```

# 応用アイデア

- **画像・ファイルの転送対応**
- **メンション機能**: @指定でZoom内の特定ユーザーに通知
- **自動翻訳機能**: 多言語チーム対応
- **チャンネル振り分け**: メッセージ内容に応じて送信先変更

# 要検討事項
今回の実装はあくまでPoCなので、実際に活用していくにあたっては権限管理や、監査、ロギング、またAPIなどのエラーハンドリングやAPIのRate Limitへのケアなどが必要です。状況に応じてご検討ください。

# まとめ
顧客サポート、社内ヘルプデスク、チーム間連携など、様々な用途で活用できるシステムです。企業のコミュニケーションツール統合に興味がある方は、ぜひ試してみてください！

---

### 参考リンク
- [Zoom API Documentation](https://developers.zoom.us/docs/api/)
- [LINE Messaging API](https://developers.line.biz/ja/docs/messaging-api/)

LINE Developersに加えて、こちらの資料は大変参考になりました。有名かもしれませんが、リンク貼っておきます。


https://mochikoastech.booth.pm/items/4717104