🌟

GASでSlackAPIを使ってファイルアップロード

2024/11/01に公開

はじめに

SlackAPIのfiles.uploadの廃止に伴い、files.getUploadURLExternalfiles.completeUploadExternalを使用してファイルアップロードしなければいけなくなりました。それをGASでやりたい話。

簡単な流れ

  1. getUploadURLExternalでアップロードするためのURLを取得
  2. 取得したURLを使ってファイルをアップロード
  3. completeUploadExternalでSlackにファイルメッセージを送信

事前に

SlackのApps管理画面でBotにfiles:writefiles:write:userの権限を付与してあげてください。Apps管理画面の「OAuth & Permissions」から権限を追加できます。(※Appの再インストール必須)
これを怠るとmissing_scopeのPermissionsエラーになります。

https://api.slack.com/apps

アップロードURL取得 (getUploadURLExternal)

まずgetUploadURLExternalAPIを使用してアップロードするためのURLとファイルIDを取得します。ファイルIDは最後のcompleteUploadExternalで使用します。

https://api.slack.com/methods/files.getUploadURLExternal

GASでSlackAPIを使用するにあたってのつまづきポイントは間違いなくContent-Typeです。GASはfetch処理がUrlFetchAppにラッピングされてて楽なときは楽なのですが、今回は叩くAPIよってContent-Typeが変わるのでpayloadへのパラメータの渡し方や認証の方法が異なりハマりポイントになってます。
getUploadURLExternalはFormDataとして送信するため、パラメータをすべて文字列に変換する必要があります。

例としてGoogleドライブのファイルをSlackへアップロードします。

// Appsのトークン
const APP_TOKEN = 'xoxb-xxxx'

// Googleドライブのファイル
const file = DriveApp.getFileById('182veQVQ5Ilnzlzv1m0183_oHezu75Yli')
const fileName = file.getName()
const fileSize = file.getSize()

let endpoint, method, contentType, headers, payload, response, result

// アップロード情報取得
endpoint = 'https://slack.com/api/files.getUploadURLExternal'
method = 'POST'
contentType = 'application/x-www-form-urlencoded'
headers = {}
payload = {
  token: APP_TOKEN,
  filename: fileName,
  length: `${fileSize}`, // 必ず文字列にする
}
response = UrlFetchApp.fetch(endpoint, { method, contentType, headers, payload })

// 結果取得
result = JSON.parse(response.getContentText())
const uploadUrl = result.upload_url
const fileId = result.file_id

getUploadURLExternalのレスポンスは以下の情報が返ってきます。

ok: true,
upload_url: 'https://files.slack.com/upload/v1/XXXXXX',
file_id: 'F0XXXXXXXXX'

注意するべき点はlengthのパラメータを文字列にする点です。これを怠るとinvalid_argumentsエラーとなります。しかもエラーメッセージが[ERROR] must provide a number [json-pointer:/length]なので「数値なのに…?」と混乱を極めます。

length: `${fileSize}`, // 必ず文字列にする

ちなみにgetUploadURLExternalはドキュメントだとmethodはGET指定ですが、特にGETでもPOSTでも問題なく動きます。

ファイルをアップロード

つぎに取得したアップロードURLを使用してファイルをアップロードします。
ここではバイナリデータをアップロードするのでpayloadに直接Blobデータを入れます。

  // ファイルアップロード
  endpoint = uploadUrl
  method = 'POST'
  contentType = 'application/x-www-form-urlencoded'
  headers = {}
  // payloadに直接Blobデータを入れる
  payload = file.getBlob()
  response = UrlFetchApp.fetch(endpoint, { method, contentType, headers, payload })
  result = response.getContentText() // レスポンスは文字列なのでパースしない

もしレスポンスを利用する場合は、JSONではなくただの文字列が返ってくるので注意してください。

OK - 53703

Slackにファイルメッセージを送信 (completeUploadExternal)

最後にcompleteUploadExternalAPIでSlackへファイルメッセージを送信します。これを実行して初めてSlack上で「ファイル」の一覧に載るようになります。

https://api.slack.com/methods/files.completeUploadExternal

  // Slackファイルメッセージ送信
  const channelId = 'CXXXXXXX'
  endpoint = 'https://slack.com/api/files.completeUploadExternal'
  method = 'POST'
  contentType = 'application/json'
  headers = { Authorization: `Bearer ${APP_TOKEN}` }  // jsonの場合は認証ヘッダーを付与
  // payloadはJSON文字列化する
  payload = JSON.stringify({
    channel_id: channelId,
    files: [{ id: fileId, title: fileName }],  // ファイル情報を配列でセット
  })
  response = UrlFetchApp.fetch(endpoint, { method, contentType, headers, payload })
  result = JSON.parse(response.getContentText())
  console.log(result)

completeUploadExternalのレスポンスは以下の情報が返ってきます。

ok: true,
files:
id: 'F0XXXXXX',
created: 1730443250,
...

パラメータに配列を含むのでContent-Typeapplication/jsonを指定します。そしてapplication/jsonの時は、tokenをbodyのパラメータとして送らずにヘッダーに認証情報として付与します。

headers = { Authorization: `Bearer ${APP_TOKEN}` }

filesパラメータにはファイル情報を配列でセットします。idにはgetUploadURLExternalで返ってきたファイルIDを指定して、titleにはファイル名を指定します。
Content-Typeapplication/jsonなので、payloadはそのまま送らずにJSON文字列化して送信します。
パラメータにthread_tsを指定すればスレッドにもメッセージを送信できます。

これでファイルアップロードは完了です。最後にソースの全体像を貼っておきます。

全体のソースコード

  // Appsのトークン
  const APP_TOKEN = 'xoxb-xxxx'

  // Googleドライブのファイル
  const file = DriveApp.getFileById('182veQVQ5Ilnzlzv1m0183_oHezu75Yli')
  const fileName = file.getName()
  const fileSize = file.getSize()

  let endpoint, method, contentType, headers, payload, response, result

  // アップロード情報取得
  endpoint = 'https://slack.com/api/files.getUploadURLExternal'
  method = 'POST'
  contentType = 'application/x-www-form-urlencoded'
  headers = {}
  payload = {
    token: APP_TOKEN,
    filename: fileName,
    length: `${fileSize}`, // 必ず文字列にする
  }
  response = UrlFetchApp.fetch(endpoint, { method, contentType, headers, payload })
  result = JSON.parse(response.getContentText())
  const uploadUrl = result.upload_url
  const fileId = result.file_id

  // ファイルアップロード
  endpoint = uploadUrl
  method = 'POST'
  contentType = 'application/x-www-form-urlencoded'
  headers = {}
  payload = file.getBlob()
  response = UrlFetchApp.fetch(endpoint, { method, contentType, headers, payload })
  result = response.getContentText() // レスポンスは文字列なのでパースしない

  // Slackファイルメッセージ送信
  const channelId = 'CXXXXXXX'
  endpoint = 'https://slack.com/api/files.completeUploadExternal'
  method = 'POST'
  contentType = 'application/json'
  headers = { Authorization: `Bearer ${APP_TOKEN}` }  // jsonの場合は認証ヘッダーを付与
  // payloadはJSON文字列化する
  payload = JSON.stringify({
    channel_id: channelId,
    files: [{ id: fileId, title: fileName }],  // ファイル情報を配列でセット
  })
  response = UrlFetchApp.fetch(endpoint, { method, contentType, headers, payload })
  result = JSON.parse(response.getContentText())

おわりに

GAS最高!

Discussion