GASでSlackAPIを使ってファイルアップロード
はじめに
SlackAPIのfiles.upload
の廃止に伴い、files.getUploadURLExternal
とfiles.completeUploadExternal
を使用してファイルアップロードしなければいけなくなりました。それをGASでやりたい話。
簡単な流れ
-
getUploadURLExternal
でアップロードするためのURLを取得 - 取得したURLを使ってファイルをアップロード
-
completeUploadExternal
でSlackにファイルメッセージを送信
事前に
SlackのApps管理画面でBotにfiles:write
やfiles:write:user
の権限を付与してあげてください。Apps管理画面の「OAuth & Permissions」から権限を追加できます。(※Appの再インストール必須)
これを怠るとmissing_scope
のPermissionsエラーになります。
アップロードURL取得 (getUploadURLExternal)
まずgetUploadURLExternalAPIを使用してアップロードするためのURLとファイルIDを取得します。ファイルIDは最後のcompleteUploadExternal
で使用します。
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上で「ファイル」の一覧に載るようになります。
// 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-Type
はapplication/json
を指定します。そしてapplication/json
の時は、tokenをbodyのパラメータとして送らずにヘッダーに認証情報として付与します。
headers = { Authorization: `Bearer ${APP_TOKEN}` }
files
パラメータにはファイル情報を配列でセットします。id
にはgetUploadURLExternal
で返ってきたファイルIDを指定して、title
にはファイル名を指定します。
Content-Type
がapplication/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