【Gas】LineBotから画像を発信する(push)

# きっかけ
Gas+lineの連携でGasからユーザに向けて画像を発信(push)したいが、Goole DriveのURLを貼るだけだと送れなかったため、あまり記事がなかったためその備忘録です。
**※ユーザからのメッセージがきてそれへの返信(reply)ではなく、LineBotからの発信(push)です。**

# TL;DR
Could Storage(Google)のREST API経由でアップロードする

# 背景
画像をGoogle Driveに保存して、LineBotからのpushによる画像の発信では、画像が表示されません。(replyならいけます、セキュリティの関係っぽい？)
そのため、今回はCloud Storageに画像をアップロードして画像を送ります。

# 手順
1. サービスアカウントを用意する 
2. バケットを作成する
3. Gasスクリプトプロパティを設定する
4. Gasでコードを書く

## 1. サービスアカウントを用意する
「APIとサービス」 => 「認証情報」 => 「サービスアカウントの管理」で新しいサービスアカウンを作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3478972/1c4d5455-893d-432a-936b-e49585abde49.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3478972/017c6414-a746-4b93-bc88-4e30d7e62908.png)
※「サービスアカウント名」さえ、入力すれば十分です。

その後、「操作」=>「鍵を管理」から、keyをjson形式で生成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3478972/7ea8f8cd-fe3b-49db-a258-2c1868303f22.png)

## 2. バケットを作成する
「Cloud Storage」=> 「バケット」からバケットを作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3478972/5cdf6ed6-5705-4d73-a9ac-be893cd943e1.png)
使う頻度等で、設定の変更が可能です。(「ローケーションの料金に１回あたりの金額が異なります)
もしURLを公開したい場合には「アクセス権の編集」=>「公開アクセス」でallUsersを追加し、権限を付与してください

## 3. Gasスクリプトプロパティを設定する
先ほど取得したjsonの鍵の値、作成したバケットの名前をスクリプトプロパティに追加します。
今回は、それぞれ、「SERVICE_ACCOUNT_KEY」、「BUCKET_NAME」という名前にしました。

## 4. Gasでコードを書く
Cloud Storageにアップロード
手順としては、鍵のjsonファイルを用いてアクセストークンを取得し、それを用いてアップロードします。
アップロードするファイルはGoogle Driveに保存されていると考え、ファイルIDを取得してください。


```typescript

const GCS_SCOPE = 'https://www.googleapis.com/auth/devstorage.full_control';
const GCS_UPLOAD_BASE_URL = 'https://www.googleapis.com/upload/storage/v1';


/**
 * サービスアカウントからアクセストークンを取得するヘルパー関数
 * @returns {string} アクセストークン
 */
function getServiceAccountAccessToken() {
  // サービスアカウントの鍵を取得
  const serviceAccountKeyJson = PropertiesService.getScriptProperties().getProperty('SERVICE_ACCOUNT_KEY');
  if (!serviceAccountKeyJson) {
    throw new Error('スクリプトプロパティにサービスアカウントキーが設定されていません。');
  }

  const serviceAccountKey = JSON.parse(serviceAccountKeyJson);
  // アクセストークンを取得します
  const service = OAuth2.createService('GCS_Service_Account')
    .setPrivateKey(serviceAccountKey.private_key)
    .setIssuer(serviceAccountKey.client_email)
    .setScope(GCS_SCOPE)
    .setSubject(serviceAccountKey.client_email) // サービスアカウント自身がアクセス主体
    .setTokenUrl('https://oauth2.googleapis.com/token');
  
  return service.getAccessToken();
}



/**
 * Cloud Storageにblob形式の画像をアップロードする
 * @return url {str} 保存した画像のURL
 */
function uploadFile(){
  /** 送信するファイルID*/
  const file = Drive.getFileById("xxxxxxxx");
  const fileName = "sampleFileName";


  const bucketName = PropertiesService.getScriptProperties().getProperty('BUCKET_NAME');
  const accessToken = getServiceAccountAccessToken();
  // エンドポイント
  const uploadUrl = `${GCS_UPLOAD_BASE_URL}/b/${bucketName}/o?uploadType=media&name=${encodeURIComponent(fileName)}`;

  // 今回はファイルの形式をBlob形式にしています。
  // 画像形式でも可能です。その場合にはContent-Typeを変える必要があります。
  const options = {
    headers: {
      'Authorization': `Bearer ${accessToken}`,
      'Content-Type': MimeType.PNG,
    },
     method: 'POST',
    payload: file.getBytes(),
    muteHttpExceptions: true
  };
  // アップロード
  const response = UrlFetchApp.fetch(uploadUrl, options);

  if(response.getResponseCode() != 200){
    Logger.log(`エラーが発生しました:  ${response.getResponseCode()}`);
    Logger.log(response.getContentText())
  }
  
  // ここでURLを取得しています。
  // バケットの公開アクセスを全員にしていない場合には取得できません。
  return `https://storage.googleapis.com/${bucketName}/${encodeURIComponent(fileName)}`;
}
```

Lineで送信する
スクリプトプロパティに「USER_ID」を設定しておいてください。
このUSER_IDはline側が持っているユーザを認識するものです。

[lineのユーザーIDについて](https://developers.line.biz/ja/docs/messaging-api/getting-user-ids/#getting-user-ids)

Lineで送信する
スクリプトプロパティに「USER_ID」を設定しておいてください。
このUSER_IDはline側が持っているユーザを認識するものです。
[lineのユーザーIDについて](https://developers.line.biz/ja/docs/messaging-api/getting-user-ids/#getting-user-ids)

```typescript
function pushImage() {
  // line側のアクセストークン
  const LINE_CHANNEL_ACCESS_TOKEN = PropertiesService.getScriptProperties().getProperty("LINE_CHANNEL_ACCESS_TOKEN");
  const userId = PropertiesService.getScriptProperties().getProperty("USER_ID");
  
  const LINE_ENDPOINT = "https://api.line.me/v2/bot/message/push";

  // urlの取得
  const url = uploadFile();

  /** 返信用のデータ */
  const postData = {
    to: userId,
    messages: [
      {
        type: "image",
        originalContentUrl: url,
        previewImageUrl: url,   
      }
    ],
  };
  
  // 送信する
  // オプションを分けるとURLFetchRequestOptionsに変換する必要があり、URLFetchRequestOptionsが今回cannot find となるため変数(option = )を定義せずに記載した。
  UrlFetchApp.fetch(LINE_ENDPOINT, {
    method: "post",
    headers: {
      "Content-Type": "application/json",
      Authorization: "Bearer " + LINE_CHANNEL_ACCESS_TOKEN,
    },
    payload: JSON.stringify(postData),
  });
}
```

これでLineBotからCloud Storage経由でユーザに画像を送信することができます。
再三ですが、reply時にはCloud Storageを使用する必要はありません。

# まとめ
今回は、LineBotからユーザに向けて画像を送信するやり方を書きました。
Cloud Storageを経由する必要があるので、使いすぎ、アクセス制限には気をつけましょう。