(JSのBuffer, Fileを理解した上で)Slack Upload APIで画像投稿を行う
🧑🏻‍💻

(JSのBuffer, Fileを理解した上で)Slack Upload APIで画像投稿を行う

Category
Author
Description
Slack bolt 2.0系で画像のアップロードする際にslack.files.uploadで画像投稿すると古いバージョンすぎてできないので、HttpClientのfetchを使用して、画像アップロードできるようにする
Published
December 6, 2022
Last Updated
Last Updated December 6, 2022
Writings
この記事は約7分で読めます

💡この記事でわかること・解決すること

Slack bolt 2.0系で画像のアップロードする際に slack.files.upload で画像投稿すると古いバージョンすぎてできないので、HttpClientのfetchを使用して、画像アップロードできるようにする

前提など

  • Slackのbot tokenの取得ができている
  • Slack API scopeのfiles:write がbot側で許可されている
  • Slack bot側から使用されること

TL;DR

👉
とりあえずコード見る
import FormData from 'form-data' import fetch, { RequestInit } from 'node-fetch' const url = 'https://slack.com/api/files.upload' function async uploadImage( buffer: Buffer, filename: string, options: { title?: string; channel?: string; thread_ts?: string } ): Promise<void> { const form = new FormData() form.append('file', buffer, { contentType: 'image/png', filename }) if (options.title) form.append('title', options.title) if (options.channel) form.append('channels', options.channel) if (options.thread_ts) form.append('thread_ts', options.thread_ts) const request: RequestInit = { method: 'POST', body: form, headers: { authorization: `Bearer ${token}`, ...form.getHeaders(), }, } try { await fetch(url, request) } catch (e) { console.error(e) console.error('cannot upload image!!') throw new Error(e) } }
今回は保存されているファイルを呼び出して投稿するとかではなく、文字列をQRコード画像化して投稿したかったのでnode-qrcodeでBuffer化した値をformとして送信しています。

Buffer, Blob, Fileについて

まず理解しないといけないのが、Buffer, Blob, Fileの関係性についてです。
この方の記事がかなりわかりやすくまとまっていますが、自己理解のため書いていきます。
  • Blob, Fileについて
    • Fileはファイルの場所を参照してファイル名、最終更新日などを取り出せます
    • Blobはバイナリを保持していて、blob.prototype.sizeでファイルサイズ確認や切り出しが出来ます
  • Buffer (ArrayBuffer)
    • Node.jsだとBufferが利用されて、JavaScriptだと2015年のECMA ScriptでArrayBufferが使われていました。なのでNodeの場合はBuffer、JSの場合はArrayBufferとして理解すればよいです
    • ArrayBufferはその名の通り、配列でバイナリ情報が埋め込まれています。
    • UnitXXArrayのXXの数字の長さでIntの入る量が変わります
  • BinaryString
    • その名の通りでBinaryが横にずらっと並んだような状態です。
  • DataURI
    • URL にデータを書けるようにする DataURI Scheme の文字列です
    • これ↓とかをURLに打ち込むと最小のgif画像が出ます
    • 👉
      data:image/gif;base64,R0lGODlhAQABAPAAAP///wAAACwAAAAAAQABAAACAkQBADs=
    • DataURIではbase64を使うことが多いのでただのバイナリー値を64文字の英数字+記号で表します。
    • btoa (binary to ascii)でbase64化、atob (ascii to binary)でバイナリ化ができるようになります。
    • 制御文字とか入らないようにしてるので同じデータを表すにも必要なデータ量がちょっと増えます
形式変換のチートシート(上記サイトより引用)
// BinaryString -> Uint8Array Uint8Array.fromBinaryString = function(str){ return Uint8Array.from(str.split(""), e => e.charCodeAt(0)) } // Uint8Array -> BinaryString Uint8Array.prototype.binaryString = function(){ return Array.from(this, e => String.fromCharCode(e)).join("") } // BinaryString -> DataURI function bStr2dataURI(b_str){ return "data:application/octet-stream;base64," + btoa(b_str) } // DataURI -> BinaryString function dataURI2bStr(data){ return atob(data.split(",")[1]) } // UintXXArray -> ArrayBuffer function toArrayBuffer(ua){ return ua.buffer } // ArrayBuffer -> Uint8Array ArrayBuffer.prototype.asUint8Array = function(){ return new Uint8Array(this) } // ArrayBuffer -> Uint16Array ArrayBuffer.prototype.asUint8Array = function(){ return new Uint16Array(this) } // ArrayBuffer -> Uint32Array ArrayBuffer.prototype.asUint8Array = function(){ return new Uint32Array(this) } // BinaryString, UintXXArray, ArrayBuffer -> Blob function toBlob(val){ return new Blob([val], {type: "application/octet-stream"}) } // Blob -> ArrayBuffer, BinaryString, DataURL, text function read(blob){ var fr = new FileReader() var pr = new Promise((resolve, reject) => { fr.onload = eve => { resolve(fr.result) } fr.onerror = eve => { reject(fr.error) } }) return { arrayBuffer(){ fr.readAsArrayBuffer(blob) return pr }, binaryString(){ fr.readAsBinaryString(blob) return pr }, dataURL(){ fr.readAsDataURL(blob) return pr }, text(){ fr.readAsText(blob) return pr }, } }

以上の形式を理解した上でコードを見る

今回は formでBufferをファイル保存することなく、サーバに送りたいのでBufferを送ります。
Buffer自体をそのまま送ると上記の内容でファイル名情報がないのでHttpClientは Content-Dispositionでファイル名情報を送れず、Slackサーバもファイル情報がないので保存ができません。
とりあえずなんかのファイルを送るから理解してくれという場合には application/octet-streamを使いましょう。分かる場合はちゃんと入れたほうが良いです。
// x form.append('file', buffer) // o form.append('file', buffer, { contentType: 'image/png', filename })
ここでは contentTypeを決め打ちしていますが、Bufferクラスにはファイルに関する情報は保存されていないので、ライブラリから出力されるcontentTypeを理解しておく必要があります。もしくはDBに入っている場合は、contentTypeとBinaryを分けて保存しておくなどする必要があります。
 
そして ファイルをappendしたあとのgetHeadersを見てみます。
console.log(form.getHeaders())
content-typeに multipart/form-data であることと boundaryに値が入っていることがわかります。
{ 'content-type': 'multipart/form-data; boundary=--------------------------12345678' }
boundaryが入っていると --------------------------12345678 で区切るとファイル情報を書き込んでサーバに理解してもらうためにあります。
こんな感じのデータが送られています。NODE_DEBUG=http,net,stream と打ってからアプリケーションを実行すると実際のHTTPのログを見れるのでもしよければ試してみてください。
--------------------------12345678 Content-Disposition: form-data; name="file"; filename="a.png" Content-Type: image/png aaaabbbbbccccc aaaabbbbbccccc aaaabbbbbccccc --------------------------12345678

🏌️‍♂️おわりに

今までファイルアップロード機能を作成するたびにここらへんのHTTPルールをちゃんと理解しないといけないなぁと思いつつ今まで逃げていました。なぜアップロードできないかをHTTPレベルで理解している人はここらへんのトラブルシューティングが早くなるのでHTTPは定期的に振り返っておくとお得です。
ちなみに最初にboltのバージョンが古すぎてとありますが、そもそもはバージョン上げるのが一番早い解決策です。要件のスケジュール上こういう風に対応しました。

HTTPの良書たち

引用(彷徨ったときのリンクたち)

Error in delayed stream : TypeError: source.on is not a function
Updated Apr 7, 2022
DataURI, Blob, File, (Array)Bufferをざっくり知る。 - Qiita
Electronで desktopCapturer をする際に、file,blob,dataUriを扱ったので、忘れないうちにメモしておきます。 desktopCapturer.getSources(options, callback) をすると、callbackに DesktopCapturerSource objects なるものが帰ってきて、 そのプロパティに NativeImageがあって、 NativeImageのインスタンスのメソッドの中に toPNGというものがあり、直接ローカルに保存するのはnode.jsの fs.writeFile で実現できたのですが、Rails5のAPIを叩いてファイルをPOSTする方法がわからずにいました。 HTMLのフォームを作成してsubmitするのはできたのですが、フォームを使わずにPOSTしたかったのです。 結論は、 FormData にfileオブジェクトをappendして、POSTすればよかったのでした。 これをするために、toDataURLメソッドからfileオブジェクトを生成しました。以下、その内容です。 dataURIや、dataURIスキーム、dataスキームのURL、などと言われるみたいです。dataスキームが先頭についているURLということでしょう。 DataURLは、 です。 DataURLのサンプルはこんな感じです。 data:image/png;base64,iVBO...(長いので以下略) srcにhttpから始まるURLを指定すると、データの読み込みにhttp通信が発生して時間がかかりますが、dataURIだと通信がいらないため、処理が早く済むのだとか。 詳しく知りたい場合は Data URI Scheme について をご覧ください。 {スキーム名}:{スキームの中身?} で表され、スキーム名には「file, blob, data, http, ftp」などがあります。 クラスです。バッファデータとコンテンツタイプデータを引数にもちます。 new Blob ( [バッファデータ], オプション ) するとBlobオブジェクトが返ってきます。ローカルにあるファイルと同じ雰囲気。 同じく、クラスです。BlobクラスはFileクラスから派生しています。Blob > File。 new File ( [バッファデータ], ファイル名, オプション ) するとFileオブジェクトが返ってきます。 desktopCapturerで取得する画像ファイルは、名前をつけてAPIにPOSTして、carrierwave経由でフォルダに保存したかったので、Fileクラスでファイル名を指定できるのは助かりました。 BlobとFileを詳しく知りたい場合は Blob と File クラスについて をご覧ください。 node.jsでは Bufferクラス、JavaScriptではMDNに ArrayBuffer のドキュメントがあります。 MDNではこのように説明されています。 ArrayBuffer は、一般的な固定長のバイナリデータのバッファを示すために使われるデータタイプです。 引用した説明文が理解不能だったので単語を調べてみると、バッファとは、データを一時的に保持するための場所のようです。 バイナリ形式のデータのことで、コンピュータが理解できるデータのようです。この文章みたいに、人間が読んでわかるデータは、テキストデータと言われるそうです。 データ型のようです。文字列型とか整数型とかブーリアン型とか。その仲間には、バイナリ型というバイナリデータを扱うデータタイプもあるようです。 base64について Data URIからBlob(File)を作成する方法 Convert Data URI to File then append to FormData 詳しく知るには、まだまだ調べることがたくさんありそうです。
DataURI, Blob, File, (Array)Bufferをざっくり知る。 - Qiita
Slack APIの使い方メモ - 新しいことにはウェルカム
以前、Slack APIを使ったのですが、久しぶりに使おうとしたら完全に使い方を忘れていたので、また忘れた時用のSlack API使い方メモです。 ちなみにAPIでやりたかったことは下記のようなことです。 基本的には、まずアプリを作成して、そのアプリをユーザーがワークスペースにインストールし、ユーザーがそのインストールされたアプリを介してAPIを使います。 Slackのデベロッパー向け機能には、Slackを操作するAPIだけでなく ユニークなURLを発行して、トークン無しでURLコールだけで投稿する機能 指定したURLにWebhookでイベント通知を受け取る機能 など、色々な機能があります。そして、何をするにもまずアプリを作成し、そのアプリをハブにして各種機能を開発していくスタイルになります。 Slack APIページに移動 [Building Slack apps]-[Create a Slack app]ボタンでアプリを作成 アプリはワークスペースにひも付きます。 上記でアプリが作成されるので、次に作成したアプリをユーザーがインストールして、APIを使えるようにします。 [Basic Information]-[Add features and functionality]-[Permissions]に移動 (「OAuth & Permissions」でも同じページに行けます) ちょっと分かりにくいのですが、移動先のページ(OAuth & Permissions)で「どのAPIを使うかの設定」と「アプリのインストール」を行います。 [Scopes]-[Select Permission Scopes]で使いたいAPIを選択 [Save Changes]ボタンで使いたいAPIを登録 例として下記のAPIを登録してみました。 [OAuth Token & Redirect URLs]-[Install App to Workspace]でユーザー(自分)にアプリをインストール すると自分にアプリが登録され、アクセストークンが発行されるので、以後このトークンを使ってAPIを呼び出します。 トークンはBearerトークンで、「Authorization」ヘッダーに「Bearer 」を設定して使います。 他の設定はAPIによってまちまちなので、APIのリファレンスに合わせて設定します。 すると、こんな感じで投稿されます。 自分が作成したアプリ一覧はSlack APIページの右上の「Your Apps」から行けます。 アプリ名をクリックすると、アプリの設定ページに移動します。 アプリ設定ページの[OAuth & Permissions]-[Scopes]で行います。 変更した後は、[OAuth & Permissions]-[OAuth Tokens & Redirect URLs]-[Reinstall App]でトークンを再発行します。 今まで発行したトークンを無効にするのは[OAuth & Permissions]-[Revoke All OAuth Tokens]-[Revoke Tokens]で行います。 インストールしたアプリをアンインストールするには、ワークスペースの「App +」から行います。 アプリそのものを削除するには、アプリ設定ページの[Delete App]-[Delete App]で行います。 API使用にあたり、どんなAPIがあって、どう使うかをリファレンスで調べる必要があります。 リファレンスの参照方法は下記のとおりです。 Slack APIページの[Reference]-[Methods]にAPI一覧があるので、そこから使いたいAPIを探します。 上記のAPI一覧からAPI名をクリックすると、URL・method・引数等の、API毎の使い方が載っています。 ...
Slack APIの使い方メモ - 新しいことにはウェルカム