Zoom Team Chat と DALL-E を組み合わせたチャットボットインテグレーション

プレゼンテーションの準備や、同僚へのチャットグループ内でのリアクションなどで、オリジナルのイラストを作りたい、と思ったことありますよね。例えば、取り扱っているプロダクトの電力消費量が減りました！ということを表すイラストが欲しい、とか、金曜の夜にトラブルが起きてまだ帰れてないからエナジードリンクが欲しい、とか。
そんな時に、こんなパートナーbotがいたら助かっちゃうかも。

![presentation.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/442226/920dcbbc-223e-8b9c-1158-d85c085e7817.gif)

## TL;DR
Zoom Team Chatで描いて欲しいイラストのプロンプトを入力すると、OpenAIのDALL-Eを使って指定したプロンプトに基づく画像を生成します。その後、生成された画像をZoom Team Chatのチャンネルに投稿するチャットボットを実装します。少し長くなりますが、複雑な実装はありませんので、ぜひ試してみてください。

## システム構成
### 概要
このチャットボットインテグレーションは以下のような構成になっています。

1. **Zoom App MarketplaceでのGeneral Appの作成**  — Botアプリの構築の前提作業
2. **Slash Commandの設定**  — チャットボットがコマンドを受け取る設定
3. **DALL-E APIとの連携**  — OpenAI APIを使用して画像を生成する処理
4. **チャットボットからのチャンネルへの送信**  — Zoom Chatbot APIを使用したメッセージ送信の処理

以下では、それぞれの部分のコードスニペットを提供しながら解説していきます。

---

## Step 1: Zoom App MarketplaceでのGeneral Appの作成
### Appの設定手順
Zoomチャットボットを作成するのは想像ほど難しくないです。ここでは、Zoom App Marketplace上でチャットボットをスタートするための手順を簡単にまとめます。

#### 1. **Zoom App Marketplaceにログイン**
   Zoom App Marketplaceにログインします。

![Screenshot 2025-01-28 at 22.03.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/442226/b91e107e-9460-44a8-62cb-00c5b8b9a52f.png)

#### 2. **アプリタイプの選択**
   「Develop」 -> 「Build App」を選択し、「General App」をタイプとして選びます。

![Screenshot 2025-01-28 at 22.05.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/442226/2980d8d7-59ee-00c8-cae6-a96cd06dacff.png)

「Admin-managed」か「User-managed」のどちらかを選ぶ必要があります。
   - **Admin-managed**: チャンネルの参加者に公開されます。
   - **User-managed**: アプリを追加したユーザーにのみ送信されます。

![Screenshot 2025-01-28 at 22.06.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/442226/5bffcb15-379d-cb60-2d16-ad548ed14831.png)

#### 3. **Slash Commandを設定**
「Features」 -> 「Surface」から「Team Chat」にチェックを入れ、「Team Chat Subscription」を有効化します。このSlash CommandはGlobalで一意である必要があることに注意してください。

![chrome-capture-2025-1-28.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/442226/4cd6d02f-91ff-7328-9fd0-d55adf276613.gif)

#### 4. **サーバーのセットアップ**
   以下はNode.jsで簡易的なサーバーを構築する例です。これにより、ZoomのBot Endpoint URLやOAuth Redirect URLを設定可能になります。

```javascript
const express = require('express');
const app = express();
const port = 8080;

// ルートエンドポイント
app.get('/', (req, res) => {
  res.send('It worked!');
});

// OAuth認証リダイレクトエンドポイント
app.get('/authorize', (req, res) => {
  console.log('Authorize request received.');
  res.redirect('https://zoom.us/launch/chat?jid=robot_' + process.env.ZOOM_BOT_JID);
});

// Botのエンドポイント
app.post('/generate-image', (req, res) => {
  console.log(req.body);
  res.send('Chatbot message received');
});

// サーバー起動
app.listen(port, () => console.log(`Server running on http://localhost:${port}`));
```

6. **ngrokを使用した公開URLの設定**
   ローカル開発環境でサーバーを外部からアクセス可能にするには、例えば[ngrok](https://ngrok.com/)を使用します。
   ```bash
   ngrok http 8080
   ```
   このコマンドを実行すると生成されるURLを以下の2つに設定します。
   - **OAuth Redirect URL**: `https://<ngrok-url>/authorize`
   - **Bot Endpoint URL**: `https://<ngrok-url>/generate-image`

`Bot Endpoint URL` を入力して、「Save」をクリックすると、`Bot JID`が生成されます。上述のコード内の環境変数として設定されている、`ZOOM_BOT_JID`にこれを設定します。
以上の設定が完了すれば、Zoom App Marketplaceでアプリを登録し、動作テストを開始できます。

まずはここまでで、いったんアクセスのテストをしてみると良いかもしれません。`Add App Now`をクリックすると、Team Chatにアプリが追加され、メッセージが届きます。
![Screenshot 2025-01-29 at 14.53.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/442226/d12c4822-a33f-09e1-2140-bd9e60a76dee.png)


---

## Step 2: DALL-E APIを使用した画像生成
次に、OpenAI APIを使ってDALL-Eに画像を作成するプロンプトを投げ、レスポンスとして返されるURLを取得するコードを書いてみましょう。事前にOpenAIのページで、API keyを準備することを忘れずに。準備したトークンは、`OPENAI_API_KEY`の環境変数として設定します。

https://platform.openai.com/api-keys

#### コードスニペット: DALL-E API連携
```javascript
const { Configuration, OpenAIApi } = require('openai');

// OpenAI APIクライアントの設定
const configuration = new Configuration({
  apiKey: process.env.OPENAI_API_KEY,
});
const openai = new OpenAIApi(configuration);

// DALL-E APIを呼び出す関数
async function generateImageUrl(prompt) {
  try {
    const response = await openai.images.generate({
      model: "dall-e-3",
      prompt: `draw an illustration of ${prompt}`,
      n: 1,
      size: "1024x1024",
    });
    console.log("Response:", response.data);
    const imageUrl = response.data[0].url;
    console.log(`Image URL: ${imageUrl}`);
    return imageUrl;
  } catch (error) {
    console.error("Error:", error.message);
    throw error;
  }
}

// テスト環境用の設定
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
```

上記コードでは、`process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";`を設定することで、テスト環境でHTTPS証明書のチェックを無効にしています。ただし、プロダクション環境ではこの設定を使用しないように注意してください。

このコードに `const prompt = "適当なイラスト用のテキスト";`を追加して実行すれば、おそらくimage urlが返却され、画像を見ることができると思います。

---
## Step 3: Bot Endpointでのリクエスト検証と環境変数について

以下は、Zoom Team Chatからのリクエストを受け取るエンドポイントのコード例です。このエンドポイントは、ZoomのWebhook認証を検証する仕組みを備えており、Endpoint URL Validationに対応しています。

### コードスニペット
```javascript
const crypto = require('crypto');

app.post("/generate-image", async (req, res) => {
    console.log("Request:", req.body);
    if (verifyWebhook(req)) {
        let response = { message: 'Authorized request from Zoom Team Chat.', status: 200 };
        console.log(response);
        if (req.body.event === 'endpoint.url_validation') {
            response = {
                message: {
                    plainToken: req.body.payload.plainToken,
                    encryptedToken: validateWebhook(req)
                },
                status: 200
            };
        } else if (req.body.event === 'bot_notification') {
            console.log("Bot Notification:", req.body.payload);
            const prompt = `draw a flat illustration of ${req.body.payload.cmd}`;
            console.log("Cmd:", prompt);
            try {
                const imageUrl = await generateImageUrl(prompt);
                sendMessageToZoom(req.body.payload.toJid, req.body.payload.accountId, imageUrl, prompt);
            } catch (error) {
                console.error("Failed to generate image:", error.message);
                res.status(500).send("Failed to generate image");
            }
        } else {
            console.log("Other event.");
        }
        console.log(response.message);
        res.status(response.status).json(response);
    } else {
        res.status(401).send("Unauthorized request");
    }
});

function verifyWebhook(req) {
    const message = `v0:${req.headers['x-zm-request-timestamp']}:${JSON.stringify(req.body)}`;
    const hashForVerify = crypto.createHmac('sha256', process.env.ZOOM_WEBHOOK_SECRET_TOKEN).update(message).digest('hex');
    const signature = `v0=${hashForVerify}`;
    return req.headers['x-zm-signature'] === signature;
}

function validateWebhook(req) {
    return crypto.createHmac('sha256', process.env.ZOOM_WEBHOOK_SECRET_TOKEN).update(req.body.payload.plainToken).digest('hex');
}
```

### Endpoint URL Validationについて

このコードは、ZoomのWebhook機能を使用する際にリクエストを検証する仕組みを提供します。`verifyWebhook`関数は、Zoomから送られてきたリクエストヘッダーの署名を検証し、正規のリクエストであることを確認します。

- **`endpoint.url_validation`イベント**: ZoomがWebhookエンドポイントを検証する際に送信するイベントです。この場合、Zoomに`plainToken`と`encryptedToken`の両方を返す必要があります。
- **`bot_notification`イベント**: Botが受け取る通知イベントです。このイベントをトリガーに、DALL-Eに画像生成をリクエストします。

### 環境変数について

一部にはすでに言及していますが、まとめると以下の環境変数が必要です。`.env`ファイルや環境設定で指定してください。

- **`OPENAI_API_KEY`**: OpenAI APIを利用するためのキー。
- **`ZOOM_BOT_JID`**: Zoom BotのJID（Bot ID）。
- **`ZOOM_WEBHOOK_SECRET_TOKEN`**: Zoom Webhookの秘密鍵。この鍵は署名検証に使用されます。
- **`ZOOM_CLIENT_ID`**: Team Chat Botのアプリに設定されているClient ID。
- **`ZOOM_CLIENT_SECRET`**: 同じくSecret。この2つは先に挙げられている`encryptedToken`を生成する際に利用します。


---

## Step 4: ZoomのチャットAPIを使用した送信
生成された画像をZoom Team Chatに送信する部分の実装について解説します。

#### リクエスト例
この範例はZoomのAPIを使用してチャットボットからメッセージを送信するものです。

```bash
curl -X POST \
  https://api.zoom.us/v2/im/chat/messages \
  -H "Authorization: Bearer YOUR_ZOOM_BOT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "robot_jid": "YOUR_BOT_JID",
    "to_jid": "YOUR_CHANNEL_OR_USER_JID",
    "account_id": "YOUR_ACCOUNT_ID",
    "content": {
      "head": {
        "text": "Hello!"
      }
    }
  }'
```
詳しい仕様はこちらを参照していただければと思いますが、基本的にはこれでメッセージを送れるはずです。

https://developers.zoom.us/docs/team-chat-apps/send-edit-and-delete-messages/

以下では、これを基本としつつStep 2で受け取った画像のURLを使って、最初に投げたプロンプトに沿った画像をチャンネルに投稿する、という部分の実装を行います。

#### 画像とプロンプトをチャンネルに投稿
```javascript
async function sendChat(rid, aid, imageUrl, prompt) {
    try {
        const authResponse = await axios.post('https://api.zoom.us/oauth/token?grant_type=client_credentials', {}, {
            headers: {
                'Authorization': `Basic ${Buffer.from(`${process.env.ZOOM_CLIENT_ID}:${process.env.ZOOM_CLIENT_SECRET}`).toString('base64')}`
            }
        });
        
        const messageResponse = await axios.post('https://api.zoom.us/v2/im/chat/messages', {
            "robot_jid": process.env.ZOOM_BOT_JID,
            "to_jid": rid,
            "account_id": aid,
            "content": {
                "head": {
                    "text": "DALL-E Image"
                },
                "body": [{
                    "type": "attachments",
                    "img_url": imageUrl,
                    "resource_url": imageUrl,
                    "information": {
                        "title": {
                            "text": "DALL-E Image"
                        },
                        "description": {
                            "text": prompt
                        }
                    }
                }]
            }
        }, {
            headers: {
                'Authorization': `Bearer ${authResponse.data.access_token}`,
                'Content-Type': 'application/json'
            }
        });
        
        console.log('Message sent successfully:', messageResponse.data);
        return messageResponse.data;
        
    } catch (error) {
        console.error('Error in sendChat:', error.response?.data || error.message);
        throw error;
    }
}
```
以上でチャンネルに投稿されるようになったはずです。一度試してみましょう。


<iframe width="560" height="315" src="https://www.youtube.com/embed/daqp1uEx02s?si=EKUWGKb3G28yDB3W" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>


---

## まとめ
Zoom Team ChatとDALL-Eを組み合わせることで、自動化や創造性の高いプラットフォームを実現できます。ここで紹介したガイドを基に、あなたのアプリに適したチャットボットを構築してみてください。

今回の実装サンプルは以下に挙げています。ご参考までに。

https://github.com/bitzed/generateImage/

---

## 参考文献

https://developers.zoom.us/blog/unsplash-chatbot-for-zoom-team-chat/

https://developers.zoom.us/docs/team-chat-apps/send-edit-and-delete-messages/

https://developers.zoom.us/docs/api/chatbot/#tag/chatbot-messages/POST/im/chat/messages

https://devforum.zoom.us/t/chatbot-im-chat-messages-error/81950