【GAS × LINEWORKS】CSV を LINEWORKS にアップロードする

前回の記事で Google スプレッドシートの内容を csv に変換して保存しました。
[【GAS】SpreadSheet を csv にして Drive に保存する](https://qiita.com/kunihiros/items/74cb1209eb3e80d669bb)

なので、今回はその csv データを LINEWORKS の[コンテンツアップロード API](https://developers.worksmobile.com/jp/document/1005025?lang=ja) を使ってアップロードしたいと思います。

マニアックー♪ (*‘∀‘)

あ、でも別に続きってわけじゃないので、前の記事を見ていなくてもご安心を。

前置きはいいから完成したコードを見たい！って方は読み飛ばして[コチラ](https://qiita.com/kunihiros/items/0a8ecc855fe721912d5f#%E3%82%B3%E3%83%BC%E3%83%89%E5%85%A8%E4%BD%93)までどーぞ。

# アップロードする手順

1. スプレッドシートの内容を csv データに変換する
2. LINEWORKS API の認証トークンを取得する
3. csv データを LINEWORKS サーバにアップロードする

おおまかに分けてこんな感じですね～。
では、さっそくやっていきましょう！

## スプレッドシートの内容を csv データに変換する

スプレッドシート内のデータを読み込んで、csv 形式の blob データを作成します。

blob は GAS 独自のオブジェクトで、ファイル名と MIME タイプの指定がついています。
Google Drive などの Google API を利用してファイルをやりとりするときによく使います。
JavaScript の Blob とは似ていますが違うものなので注意です。(｀・ω・´)

ではでは、その blob データを作成するコードです。

```js
function uploadLw(){
  const csvData = loadData();
  const blob = createBlob(csvData);
}

function loadData() {
  const data = SpreadsheetApp.getActiveSheet().getDataRange().getValues();
  const csv = data.map((value) => { return value.join() + "\r\n" }).join('');
  return csv
}

function createBlob(csv) {
  const fileName = 'テスト.csv'
  const contentType = 'text/csv';
  const charset = 'utf-8';
  const blob = Utilities.newBlob('', contentType, fileName).setDataFromString(csv, charset);
  return blob;
}
```

ちょこっと解説をば。

`function loadData()` でスプレッドシートから読み込んだデータは二次元配列になっています。
`[{"セルA1","セルB1","セルC1"},{"セルA2","セルB1","セルC1"},{....}]`

なので、まずは一列ずつカンマ区切りされたテキストにします。

```js
data.map((value) => { return value.join() + "\r\n" })
```

`.map` で要素を呼び出して `.join` で結合しています。最後に改行コードですね。
この時点でのデータの中身を見るとこんな感じです。
`{"セルA1,セルB1,セルC1\r\n","セルA2,セルB1,セルC1\r\n","...."}`

これをさらに `.join` させます。
一つだけ注意ですが、`.join` は引数を何も入れないと自動的にカンマ `,` で区切りますので、行を結合させるときには区切り文字を使わないよう `.join("")` と空文字を指定します。

データの中身を見てみると、String データになっています。
`"セルA1,セルB1,セルC1\r\nセルA2,セルB1,セルC1\r\n...."`

あとはこの String データを `Utilities.newBlob().setDataFromString()` で blob データに変換すれば OK！
csv 形式の blob データの完成です！

## LINEWORKS API の認証トークンを取得する

こんなマニアックな記事を読んでいる方々には釈迦に説法かと思いますので、解説は省略させていただきます。
いちおう、私が作ったライブラリを使うと簡単にトークン発行できますよー、と宣伝をしておきますｗ
[LINEWORKS ライブラリを登録する](https://qiita.com/kunihiros/items/a94221ad7c9f4de84cf8#3-%E3%83%A9%E3%82%A4%E3%83%96%E3%83%A9%E3%83%AA%E3%82%92%E7%99%BB%E9%8C%B2%E3%81%99%E3%82%8B)

コードはこんな感じです。

```js
  const obj = {
    "apiId" : "API ID",
    "consumerKey" : "Server API Consumer Key",
    "serverId" : "Server List(ID登録タイプ) の ID",
    "privateKey" : "Server List(ID登録タイプ) の認証キー",
    "botNo" : "botNo"
  };
  const token = LINEWORKS.getToken(obj);
```

## csv データを LINEWORKS サーバにアップロードする

いよいよ今回、一番苦戦したところ。
LINEWORKS の[コンテンツアップロード API](https://developers.worksmobile.com/jp/document/1005025?lang=ja) を使ってアップロードしたいと思います。

### コンテンツアップロード API の Content-Type

コンテンツアップロード API の **Content-Type** は `multipart/form-data` が指定されています。
複数の種類のデータを一度に扱える形式で、ファイルアップロードではよく利用される形式です。

`multipart/form-data` を使用するためには `Content-Type` で `boundary` を指定し、データは `payload` にセットします。

```js
  const options = {
    "contentType": "multipart/form-data; boundary=" + boundary,
    "payload": payload
  }
```

では、`boundary` と `payload` の中身を作っていきましょう。

### boundary

`boundary` は区切り文字なので、中身はなんでも良いです。(｀・ω・´)
まぁ、誰の何のデータかわかりやすい文字列にする、くらいですかね。
`ex) WebKitFormBoundaryxxxxyz`

詳しく知りたい方はこちらの記事をご参考ください。
[【HTTP】multipart/form-data の boundary って何ぞや？](https://qiita.com/kunihiros/items/e11daacfd62d85d83c06)

### payload

こっちはちょっと複雑です。
実際に送るデータの中身を記述するのですが、色々とお作法があります。

まずは、`payload` を作成するコードを見てみましょう。

```js
function uploadLw(){
  const csv = loadData();
  const blob = createBlob(csv);
  
  const boundary = "WebKitFormBoundaryxxxxyz";
  const headersText =  "--" + boundary + "\r\n"
    + "Content-Disposition: form-data; name=\"resourceName\"; filename=\"" + blob.getName() + "\"\r\n"
    + "Content-Type: " + blob.getContentType() + "\r\n\r\n";
  const footersText = "--" + boundary + "--\r\n";

  const payloadHeaders = Utilities.newBlob(headersText).getBytes();
  const payloadBody = blob.getBytes();
  const payloadFooters = Utilities.newBlob(footersText).getBytes();
  const payload = payloadHeaders.concat(payloadBody).concat(payloadFooters);
}
```

`payload` は見ていただいた通り「ここからがデータだよ！」という宣言をしている `Header` 部分、実データ部分、「ここで終わりだよ！」という宣言をしている `Footer` 部分に分かれています。

まずは、`Header` 部分のテキストデータから作成していきます。

#### payloadHeadersText

お作法として `--` から始まり、`boundary` を書いたら改行します。
次の行には `Content-Disposition: form-data;` と記載して、これは `form-data` だヨって明示します。

次に `name` を記載するのですが、LINEWORKS のドキュメントで `name` は `resourceName` と記載するように指示されているので注意しましょう。
好きな名前じゃだめですよ！('ω')

そうしたら次に `filename` を記載します。
先ほど作成した `blob` ファイルに `filename` は設定していますので `blob.getName()` で取得して設定して、改行します。

最後に送信するデータの `Content-Type` を記載します。
これも先ほど作成した `blob` ファイルに設定してありますので `blob.getContentType()` で取得して設定します。

```js
  const headersText =  "--" + boundary + "\r\n"
    + "Content-Disposition: form-data; name=\"resourceName\"; filename=\"" + blob.getName() + "\"\r\n"
    + "Content-Type: " + blob.getContentType() + "\r\n\r\n";
```

#### payloadFootersText

同じく、お作法として `--` から始まり、`boundary` を書いたら `--` で閉じて、最後に改行します。これは簡単ですね。

```js
  const footersText = "--" + boundary + "--\r\n";
```

#### blob データを作成してデータストアを取得する

テキストデータができたら `blob` 形式に変換し、`.getBytes()` を使用してデータストアを取得します。

```js
  const payloadHeaders = Utilities.newBlob(headersText).getBytes();
  const payloadBody = blob.getBytes();
  const payloadFooters = Utilities.newBlob(footersText).getBytes();
```

最後に `.concat()` で結合して完成です！

```js
  const payload = payloadHeaders.concat(payloadBody).concat(payloadFooters);
```

### コンテンツアップロード API を Request する

データが揃ったので、いよいよ API を Request します！

```js
  const headers = {
    "consumerKey": obj.consumerKey,
    "Authorization": "Bearer " + LINEWORKS.getToken(obj),
    "x-works-apiid": obj.apiId
  }
  
  const options = {
    "method": "post",
    "headers": headers,
    "contentType": "multipart/form-data; boundary=" + boundary,
    "payload": payload,
    "muteHttpExceptions": true
  };

  const uri = "http://storage.worksmobile.com/openapi/message/upload.api";
  const response = UrlFetchApp.fetch(uri, options);
  const resHeaders = response.getHeaders();
  Logger.log("x-works-resource-id: " + resHeaders["x-works-resource-id"]);
```

ログにアップロードしたファイルの `resourceId` を表示するようにしているので、ログを確認します。

![1603098671.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/363367/def3eb41-160d-91b5-1e10-e013eea02fa9.png)

成功ですね！ヾ(*´∀｀*)ﾉ
この `resourceId` を使って Bot でファイル送信ができます。
[【LINEWORKS API】新機能！BOT でファイルを送受信！](https://qiita.com/kunihiros/items/725b917b2952c5978d3f#%E5%88%A5%E3%81%AE%E3%83%A1%E3%83%B3%E3%83%90%E3%83%BC%E3%81%AB-file-%E3%82%92%E9%80%81%E4%BF%A1)

もちろん、コンテンツダウンロード API を使ってダウンロードすることもできます。

ちなみにこちらの記事でも書きましたが、`resourceId` は `body` ではなく `headers` の中に入っているのでご注意くださいね～(; ･`д･´)
[LINEWORKS のコンテンツアップロード API を使う](https://qiita.com/kunihiros/items/9816fa1860613b3b1a3a#resource-id-%E3%81%AF-headers-%E3%81%AE%E4%B8%AD%E3%81%AB)

# コード全体

最後にコード全体をさらしておきます。

```js
const obj = {
  "apiId" : "API ID",
  "consumerKey" : "Server API Consumer Key",
  "serverId" : "Server List(ID登録タイプ) の ID",
  "privateKey" : "Server List(ID登録タイプ) の認証キー",
  "botNo" : "botNo"
};

function uploadLw(){
  const csv = loadData();
  const blob = createBlob(csv);
  
  const boundary = "WebKitFormBoundaryxxxxyz";
  const headersText =  "--" + boundary + "\r\n"
    + "Content-Disposition: form-data; name=\"resourceName\"; filename=\"" + blob.getName() + "\"\r\n"
    + "Content-Type: " + blob.getContentType() + "\r\n\r\n";
  const footersText = "--" + boundary + "--\r\n";

  const payloadHeaders = Utilities.newBlob(headersText).getBytes();
  const payloadBody = blob.getBytes();
  const payloadFooters = Utilities.newBlob(footersText).getBytes();
  const payload = payloadHeaders.concat(payloadBody).concat(payloadFooters);

  const headers = {
    "consumerKey": obj.consumerKey,
    "Authorization": "Bearer " + LINEWORKS.getToken(obj),
    "x-works-apiid": obj.apiId
  }
  
  const options = {
    "method": "post",
    "headers": headers,
    "contentType": "multipart/form-data; boundary=" + boundary,
    "payload": payload,
    "muteHttpExceptions": true
  };

  const uri = "http://storage.worksmobile.com/openapi/message/upload.api";
  const response = UrlFetchApp.fetch(uri, options);
  const resHeaders = response.getHeaders();
  Logger.log("x-works-resource-id: " + resHeaders["x-works-resource-id"]);
}

function loadData() {
  const data = SpreadsheetApp.getActiveSheet().getDataRange().getValues();
  return data.map((value) => { return value.join() + "\r\n" }).join('');
}

function createBlob(csv) {
  const fileName = 'テスト.csv'
  const contentType = 'text/csv';
  const charset = 'utf-8';
  const blob = Utilities.newBlob('', contentType, fileName).setDataFromString(csv, charset);
  return blob;
}
```

# おわりに

ここまでお付き合いいただきありがとうございました。

ようやく GAS でアップロード API 使えた！
GoogleAppsScript で `multipart/form-data` 形式の記述に関する知見が物凄く少ないんですよ！
やる人、少ないのかな？
それとも、書くまでもないことなのかな？？( ﾟДﾟ)

私がわからなくて検索したとき困ったので、この記事が誰かの役に立てれば幸いです。

ではまた！(^^)/

# 参考にさせていただきましたm(_ _)m

[MDN web docs - Array.prototype.join()](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/join)
[Review of My Life - multipart/form-dataのファイルをGoogle Apps Scriptから送信する](https://review-of-my-life.blogspot.com/2018/03/multipartform-data-google-apps-script.html)