Google Business Profileのレビュー投稿を検知する機能作成

# Google Business Profile APIs 通知([URL](https://developers.google.com/my-business/reference/notifications/rest/v1/accounts?hl=ja))

主にGoogle Business Profile APIsの通知設定について記述してます。
GBP公式ドキュメントを参照していましたが、設定方法がわかりにくかったので記事にしました。

APIでの通知設定を記載します。

## 使うもの
- TypeScript
- node
- Google Cloud Platform
    - IAM
    - Pub/Sub (Google Cloud Platform)
- Google Business Profile APIs
    - 下記に記載してます
- AWS (省略します)
    - API Gateway
        - Lambda
            - 認証
            - 受け取った際の処理



## 各種設定

1. Google Cloud Platform
    1.  IAM
        1.  PubSub用のサービスアカウントを作る
    2. Pub/Sub
        1. トピックを作る
            1. 権限ロール・プリンシバルを追加
                1. Pub/Sub パブリッシャー権限以上のものを付与
            1. トピックの名前をコピーしておく
        1. サブスクリプションを作る/編集する 
            1. 配信タイプ
                1. Push
            2. エンドポイント
                1. 任意のサーバー
            1. 認証
                1. 有効
                1. サービスアカウントIAMで作ったもの
            1. その他はデフォルト設定
1. Google Business Profile APIs通知設定
    1.  公式ドキュメント参照


<hr>

### APIリクエスト用のアクセストークン取得
```/lib/google.ts
import { OAuth2Client } from "google-auth-library";

/**Googleアクセストークン取得 */
export const getGoogleAccessToken = async () => {
  try {
    const oauth2Client = new OAuth2Client(
      process.env.GOOGLE_CLIENT_ID,
      process.env.GOOGLE_CLIENT_SECRET
    )
    oauth2Client.setCredentials({
      refresh_token: process.env.GOOGLE_REFRESH_TOKEN
    })
    const response = await oauth2Client.getAccessToken()
    return response.token
  } catch (error) {
    return null
  }
}
```

### 簡易型宣言ファイル
```/types/google.d.ts
/** 
 * Google My Business Notification Setting Data
 * @property {string} [name] 通知名 (任意)
 * @property {string} pubsubTopic 通知を送信する Pub/Sub トピック
 * @property {(
* * "NEW_REVIEW" //新規レビュー
* * | "UPDATED_REVIEW" //レビュー更新
* * | "NEW_CUSTOMER_MEDIA" //Googleマップのユーザーによって、新しいメディア アイテムがビジネスに追加
* * | "NEW_QUESTION" //新しい質問がビジネスに追加
* * | "UPDATED_QUESTION" //ビジネスに関する質問が更新
* * | "NEW_ANSWER" //新しい回答がビジネスに追加
* * | "UPDATED_ANSWER" //ビジネスの回答が更新
* * | "GOOGLE_UPDATE" //ビジネス情報に Google による変更
* * | "DUPLICATE_LOCATION" //位置情報メタデータの重複するビジネス情報フィールドに変更
* * | "VOICE_OF_MERCHANT_UPDATED" //ビジネスの Voice of Merchant（VOM）ステータスの更新
* )[]} notificationTypes 通知の種類の配列
*  @example
* ```javascript
* const notificationData: NotificationData = {
*   name: 'accounts/{accountID}/notificationSetting',
*   pubsubTopic: 'projects/my-project/topics/reviews',
*   notificationTypes: ['NEW_REVIEW'],
* };
* ```
* */
interface _BusinessNotificationData {
  name?: string
  pubsubTopic: string
  notificationTypes: (
    | "NEW_REVIEW"
    | "UPDATED_REVIEW"
    | "NEW_CUSTOMER_MEDIA"
    | "NEW_QUESTION"
    | "UPDATED_QUESTION"
    | "NEW_ANSWER"
    | "UPDATED_ANSWER"
    | "GOOGLE_UPDATE"
    | "DUPLICATE_LOCATION"
    | "VOICE_OF_MERCHANT_UPDATED"
  )[]
}
```

### import
```/notification/route.ts
import { logger } from "@/lib/aws/amplifyConfig"
import { getGoogleAccessToken } from "@/lib/google"
import { _BusinessNotificationData } from "@/types/google"
import { NextRequest, NextResponse } from "next/server"
```

### 通知設定を取得する

```/notification/route.ts
export const GET = async (req: NextRequest) => {
  try {
    const googleAccessToken = await getGoogleAccessToken()
    const accountId = process.env.GOOGLE_ACCOUNT_ID
    const url = `https://mybusinessnotifications.googleapis.com/v1/accounts/${accountId}/notificationSetting`

    const response = await fetch(url, {
      method: "GET",
      headers: {
        "Content-Type": "application/json",
        "Authorization": `Bearer ${googleAccessToken}`
      },
    })

    if (!response.ok) {
      logger.warn("[GBP通知][情報取得][失敗]")
      return NextResponse.json({ message: "GoogleMyBusinessから情報取得できませんでした" }, { status: 400 })
    }

    const myBusinessNotifications = await response.json()
    logger.debug("[GBP通知][情報取得][成功]")
    return NextResponse.json(
      { myBusinessNotifications },
      { status: 200 }
    )

  } catch (error) {
    logger.error("[GBP通知][情報取得][エラー]")
    return NextResponse.json(
      { message: "Failed to fetch data", error: error },
      { status: 500 }
    )
  }
}   
```

### 通知設定を更新する

```/notification/route.ts
/** 値を受け取って更新する仕様にしています*/
export const PATCH = async (req: NextRequest) => {
  try {
    const { notificationTypes }: { notificationTypes: _BusinessNotificationData["notificationTypes"] } = await req.json()
    const googleAccessToken = await getGoogleAccessToken()
    const accountId = process.env.GOOGLE_ACCOUNT_ID
    /**　公式ドキュメントにはpubsubTopicの記載がないが必要*/
    const updateMask = "notificationTypes,pubsubTopic"
    const url = `https://mybusinessnotifications.googleapis.com/v1/accounts/${accountId}/notificationSetting?updateMask=${updateMask}`
    /** 常にレビュー、レビュー更新、GOOGLEによる更新を検知できるように設定しています */
    const data: _BusinessNotificationData = {
      name: `accounts/${accountId}/notificationSetting`,
      pubsubTopic: process.env.PUBSUB_TOPIC_NAME,
      notificationTypes: notificationTypes || [
        'NEW_REVIEW',
        'UPDATED_REVIEW',
        'GOOGLE_UPDATE'
      ],
    }

    const response = await fetch(url, {
      method: "PATCH",
      headers: {
        "Content-Type": "application/json",
        "Authorization": `Bearer ${googleAccessToken}`
      },
      body: JSON.stringify(data)
    })

    if (!response.ok) {
      logger.warn("[GBP通知][情報更新][失敗]")
      return NextResponse.json({ message: "GoogleMyBusinessから情報取得できませんでした" }, { status: 400 })
    }
    const myBusinessNotifications = await response.json()
    logger.debug("[GBP通知][情報更新][成功]")

    return NextResponse.json(
      { myBusinessNotifications },
      { status: 200 }
    )


  } catch (error) {
    logger.warn("[GBP通知][情報更新][エラー]", error)
    return NextResponse.json(
      { message: "Failed to fetch data", error: error },
      { status: 500 }
    )
  }
}
```

ここで設定したものが検知された際に、PubSubトピックが実行されます。