GASとZoom apiを使ってZoom AI Companionを取得してみた

業務でzoom apiを使って要約を取得したのでポイントをまとめます。

# 実装したいこと
* **本日分のZoom AI Companion(要約)を取得して要約文をSlackに投げる。**

そのために以下のことを実装しました。
+ meeting uuidの取得
+ meeting summaryの取得
+ slackに投稿

## ZoomOauth認証
基本的には以下のようにコードを書いて[zoom app marketplace](https://marketplace.zoom.us/)でアプリを作成すれば認証できます。
GAS側のコード
+ 実行を担うdoGet関数
```
var Properties = PropertiesService.getScriptProperties();
var Client_Id = Properties.getProperty('CLIENT_ID');
var Client_Secret = Properties.getProperty('CLIENT_SECRET');
/**
 * Authorizes and makes a request to the Zoom API.
 */
function doGet() {
  var service = getZoomService_();
  Logger.log(service);
  if (!service.hasAccess()) {
    var authorizationUrl = service.getAuthorizationUrl();
    return HtmlService.createHtmlOutput("Click <a href='" + authorizationUrl + "' target='_blank'>here</a> to authorize.");
  } else {
    // User is authorized
    return HtmlService.createHtmlOutput("Authentication successful!");

  }
}
```
* OAuth2認証のセットアップ
```
function getZoomService_() {
  return OAuth2.createService('zoomreport')
      .setAuthorizationBaseUrl('https://zoom.us/oauth/authorize')
      .setTokenUrl('https://zoom.us/oauth/token')
      .setClientId(Client_Id)
      .setClientSecret(Client_Secret)
      .setCallbackFunction('authCallback') 
      .setPropertyStore(PropertiesService.getScriptProperties())
      .setRedirectUri('https://script.google.com/macros/d/{GASのスクリプトID}/usercallback')
      .setScope('{必要なスコープを追加}') //複数ある場合は「,」でスペースを開けずに繋げる
      .setTokenHeaders({
        'Authorization': 'Basic ' +
          Utilities.base64Encode(Client_Id + ':' + Client_Secret),
      });
}
```
* callback関数
```
function authCallback(request) {
  var service = getZoomService_();
  var authorized = service.handleCallback(request);
  if (authorized) {
    return HtmlService.createHtmlOutput('Success! Access Token: ' + service.getAccessToken());
  } else {
    return HtmlService.createHtmlOutput('Denied.');
  }
}
```
* リセット関数：スコープなどを変更した際にリセットして再認証する
```

function reset() {
  getZoomService_().reset();
}

```

Zoom app 側の設定は、適切なscopeを追加すれば問題ないと思います。
以下の記事などを参考にしました。

https://qiita.com/yosuke-sawamura/items/514e791afb4cb02fd956

https://cybozu.dev/ja/common/tips/authentication/utilizing-kintone-api-from-google-apps-script-with-oauth2.0/


### 認証の流れ
GASで認証コードを書く+サービスからoauthを追加する　→ デプロイ(WEBアプリ) → zoom appの設定をする　→ デプロイされたWEBアプリのURLを踏むと認証できます。

## meeting UUIDの取得
UUIDとは、会議ごとに与えらえる一意のIDのこと。
例えば毎日行われるAという会議のUUIDを取得する場合、meetingIDは毎日同じですが、**UUIDは異なります**。
UUIDの例：　aDYlohsHRtCd4ii1uC2+hA==


今回meetingIDとは別にUUIDを取得するのは、zoon apiで要約を取得する際にUUIDが必要となるからです。

[Get a meeting summary](https://developers.zoom.us/docs/api/rest/reference/zoom-api/methods/#operation/Getameetingsummary)

UUIDを直接取得することも考えましたが、取得したいミーティングが**scheduled**されていないものだったのでダメでした。(https://developers.zoom.us/docs/api/rest/reference/zoom-api/methods/#operation/meetings) 

そのため、meetingID →　UUID　へと変換する必要があります。
(meetingIDの習得は省略します)

そのためのメソッドは、[Get Past Meeting Details](https://developers.zoom.us/docs/api/rest/reference/zoom-api/methods/#operation/pastMeetingDetails)　です。
meetingIDまたはUUIDをパラメータとしています。

>meetingId: The meeting's ID or universally unique ID (UUID)
>If you provide a meeting ID, the API will return a response for the latest meeting instance.
>If you provide a meeting UUID that begins with a / character or contains the // characters, you must double encode the meeting UUID before making an API request.

コードは、以下のようになります。
```
function getMeetingUUID(meetingId) {
  try {
    const meetingDetails = sendZoomApiRequest(`/past_meetings/${meetingId}`);
    return meetingDetails.uuid;
  } catch (error) {
    console.error('Error fetching meeting UUID:', error);
    throw error;
  }
}
```

ここで、apiの取得について理解が浅かったのでまとめてみました。

### APIリクエストについて
https://qiita.com/kou1121/items/d9a9af62e2325741c2ec

この記事を参考にまとめます。
**UrlFetchApp.fetch(URL[, パラメータ])** という構文を用います。
fetch: urlに対してパラメータparamsを渡してHTTPリクエストを行う。
私のコードでは、
```
const url = `${ZOOM_API_BASE_URL}${endpoint}`;
const options = {
    method: method,
    headers: {
      'Authorization': 'Bearer ' + accessToken,
      'Content-Type': 'application/json'
    },
    
  };
const response = UrlFetchApp.fetch(url, options);
```
としています。
optionsでは、method(GET)とheaders(リクエストのHTTPヘッダー)を指定してます。

その後、文字型にするために以下の変換を行います。
```
JSON.parse(response.getContentText());
```
これにより、文字列になります。

以下は、zoom apiにリクエストする機能を汎化した関数です。
```
const ZOOM_API_BASE_URL = 'https://api.zoom.us/v2';
function sendZoomApiRequest(endpoint, method = 'GET') {
  const accessToken = getZoomAccessToken();
  const url = `${ZOOM_API_BASE_URL}${endpoint}`;
  const options = {
    method: method,
    headers: {
      'Authorization': 'Bearer ' + accessToken,
      'Content-Type': 'application/json'
    },
    
  };

  try {
    const response = UrlFetchApp.fetch(url, options);
    if (response.getResponseCode() === 200) {
      console.log(JSON.parse(response.getContentText()));
      return JSON.parse(response.getContentText());
    } else {
      throw new Error(`API request failed with status ${response.getResponseCode()}: ${response.getContentText()}`);
    }
  } catch (error) {
    console.error(`Error in Zoom API request to ${endpoint}:`, error);
    throw error;
  }
}
```
ここまでで、UUIDが取得できました。

残りは、UUIDからサマリーを取得してみます。

## meeting summaryの取得

https://developers.zoom.us/docs/api/rest/reference/zoom-api/methods/#operation/Getameetingsummary

ドキュメントを参考に実装します。
apiリクエストをすると以下のような形式でデータが返ってきます。
```
{
  "meeting_host_id": "30R7kT7bTIKSNUFEuH_Qlg",
  "meeting_host_email": "jchill@example.com",
  "meeting_uuid": "aDYlohsHRtCd4ii1uC2+hA==",
  "meeting_id": 97763643886,
  "meeting_topic": "My Meeting",
  "meeting_start_time": "2019-07-15T23:24:52Z",
  "meeting_end_time": "2020-07-15T23:30:19Z",
  "summary_start_time": "2019-07-15T23:24:52Z",
  "summary_end_time": "2020-07-15T23:30:19Z",
  "summary_created_time": "2019-07-15T23:24:52Z",
  "summary_last_modified_time": "2020-07-15T23:30:19Z",
  "summary_title": "Meeting summary for my meeting",
  "summary_overview": "Meeting overview",
  "summary_details": [
    {
      "label": "Meeting overview",
      "summary": "Meeting overview"
    }
  ],
  "next_steps": [
    "step1"
  ],
  "edited_summary": {
    "summary_details": "Meeting overview",
    "next_steps": [
      "step1"
    ]
  }
}

```
今回必要な情報は、summary_overview(概要)・summary_details(詳細)・next_steps(今後の方針など)です。
以下がコードです。基本的には、UUIDを取得したのと同じ要領で取得すれば行けます。
**AI Companion機能がオンになっていることが必要です**
```
function getSummaryFromZoom(uuid) {
  let summaries = {};

  try {
    const encodedUUID = encodeUUID(uuid);
    const summary = sendZoomApiRequest(`/meetings/${encodedUUID}/meeting_summary`);
    // 全体のレスポンスを確認するため、ログ出力
    //console.log('Summary response:', summary);
    
    summaries = {
      'overview': summary.summary_overview,
      'details': summary.summary_details,
      'nextstep': summary.next_steps
    };
    console.log(summaries.details);
    return summaries;
  } catch (error) {
    console.error('Error fetching meeting summary:', error);
    throw error;
  }
}
```
また、/で始まる場合と//が含まれる場合二重エンコードする必要があるので以下の関数を作成しました。

```
// UUIDを必要に応じて二重エンコードする関数
function encodeUUID(uuid) {
  if (uuid.startsWith('/') || uuid.includes('//')) {
    return encodeURIComponent(encodeURIComponent(uuid));
  }
  return encodeURIComponent(uuid);
}
```

ここまでで、主要な関数を作成し終わりました。
slackに投稿する機能は、以下の記事が参考になりました。

https://ex-ture.com/blog/2023/07/27/%E3%80%90google-app-script%E3%80%91gas%E3%82%92%E5%88%A9%E7%94%A8%E3%81%97%E3%81%A6slack%E3%81%AB%E6%8A%95%E7%A8%BF%E3%81%99%E3%82%8Bbot%E3%82%92%E4%BD%9C%E3%82%8B/

肝となるのは、APIリクエストの部分でした。APIリクエストについての理解が浅いので今後勉強していきたいです。