ENECHANGE VPoTの岩本 (iwamot) です。
2024年4月~6月の四半期から、社内I/O Dayを試験的に始めています。「プロダクト開発や事業課題の解決に活用できそうな技術について、集中的にインプット/アウトプットする日を宣言する制度」のことです(くわしくは別の機会にお伝えします)。
多くのエンジニアが本日5月15日にI/O Dayを宣言していたので、ぼくも乗っかってみました。お祭り感があって、いいですね。
ぼくが取り組んだテーマは、Slackのfiles.upload API廃止対応でした。2025年3月までに、新しいAPIに移行する必要があるんですよね。まだ時間はあるものの、ボリューム的にちょうどよさそうなので選びました。
今回の記事では、Google Apps Scriptからの呼び出し方をどう変えたのかご紹介します。これから廃止対応を進める方の参考になれば幸いです。
変更前のコード
まず、変更前のコードをご紹介します*1。とあるクラスの uploadFile
メソッドです。files.upload APIをシンプルに呼んでいます。
/** * ファイルをアップロードする * * @param {Blob} fileBlob - ファイル * @param {String} filename - ファイル名 * @return {String} アップロードしたファイルURL */ uploadFile(fileBlob, filename) { const options = { payload: { token: this.oauthToken, file: fileBlob, filename: filename, }, }; const response = UrlFetchApp.fetch( "https://slack.com/api/files.upload", options ); const responseBody = response.getContentText(); const data = JSON.parse(responseBody); if (!data.ok) { throw new Error(data); } return data.file.permalink; }
新しいアップロード手順
新しいAPIでのファイルアップロード手順は、下記のとおりです。
- files.getUploadURLExternal API を呼び出してファイルアップロードに使用する
https://files.slack.com/upload/v1/...
URL とファイル ID を取得するhttps://files.slack.com/upload/v1/...
URL に POST リクエストを送信してファイルをアップロードする- files.completeUploadExternal API にファイル ID に加えて チャンネル ID などを送信して、アップロード処理を完了させる
変更後のコード
変更後のコードは下記のようになりました。新しいアップロード手順を素直に実装した形です。クラスにメソッドを3つ追加しています。
/** * ファイルをアップロードする * * @param {Blob} fileBlob - ファイル * @param {String} filename - ファイル名 * @return {String} アップロードしたファイルURL */ uploadFile(fileBlob, filename) { const [fileUploadUrl, fileId] = this.getFileUploadUrl( filename, fileBlob.getBytes().length ); const options = { method: "post", payload: fileBlob, }; const response = UrlFetchApp.fetch(fileUploadUrl, options); Logger.log(response); this.completeFileUpload(fileId); return this.getFilePermalink(fileId); } /** * ファイルアップロード用URLを取得する * * @param {String} filename - ファイル名 * @param {Number} length - ファイルサイズ * @return {String[]} ファイルアップロード用URL、ファイルID */ getFileUploadUrl(filename, length) { const options = { method: "get", headers: { Authorization: "Bearer " + this.oauthToken, }, payload: { filename: filename, length: length.toString(), }, }; const response = UrlFetchApp.fetch( "https://slack.com/api/files.getUploadURLExternal", options ); Logger.log(response); const responseBody = response.getContentText(); const data = JSON.parse(responseBody); if (!data.ok) { throw new Error(data); } return [data.upload_url, data.file_id]; } /** * ファイルアップロード処理を完了する * * @param {String} fileId - ファイルID */ completeFileUpload(fileId) { const options = { method: "post", headers: { Authorization: "Bearer " + this.oauthToken, }, contentType: "application/json", payload: JSON.stringify({ files: [{ id: fileId }], }), }; const response = UrlFetchApp.fetch( "https://slack.com/api/files.completeUploadExternal", options ); Logger.log(response); const responseBody = response.getContentText(); const data = JSON.parse(responseBody); if (!data.ok) { throw new Error(data); } } /** * ファイルのURLを取得する * * @param {String} fileId - ファイルID * @return {String} ファイルのURL */ getFilePermalink(fileId) { const options = { method: "get", headers: { Authorization: "Bearer " + this.oauthToken, }, payload: { file: fileId, }, }; const response = UrlFetchApp.fetch( "https://slack.com/api/files.info", options ); Logger.log(response); const responseBody = response.getContentText(); const data = JSON.parse(responseBody); if (!data.ok) { throw new Error(data); } return data.file.permalink; }
いくつか補足します。
整数化はtoStringで
getFileUploadUrl(filename, length) { const options = { method: "get", headers: { Authorization: "Bearer " + this.oauthToken, }, payload: { filename: filename, length: length.toString(),
lengthを文字列に変換しているのは、数値型のままだと 12345.0
のような値がPOSTされ、エラーになるためです。
配列を含むペイロードはJSON.stringifyで
completeFileUpload(fileId) { const options = { method: "post", headers: { Authorization: "Bearer " + this.oauthToken, }, contentType: "application/json", payload: JSON.stringify({ files: [{ id: fileId }], }), };
JSON.stringifyしているのは、files.completeUploadExternal APIのペイロードで配列を指定するのに必要なためです。contentTypeにも要注意。くわしくは下記の記事をご参照ください。
また、トークンをヘッダに移動したのは、JSONペイロード内で渡しても無視されてしまうためです。
トークンはヘッダに含めるようドキュメントでも推奨されています。
We prefer tokens to be sent in the
Authorization
HTTP header of your outbound requests.
files.info APIを呼ぶならfiles.readの許可を
getFilePermalink(fileId) { const options = { method: "get", headers: { Authorization: "Bearer " + this.oauthToken, }, payload: { file: fileId, }, }; const response = UrlFetchApp.fetch( "https://slack.com/api/files.info", options );
多くのユースケースでは、アップロードしたファイルのURLが必要になることでしょう。今回のコードもそうでした。
ファイルのURLを取得するには、files.info APIが使えます(他の方法もあるかもしれませんが、調べていません)。
ただし、files.info APIを呼び出すには files.read
スコープが必要です。files.write
だけではエラーになります。
おわりに
以上、Slackのfiles.upload API廃止に備えて、Google Apps Scriptのコードに手を入れた例をご紹介しました。
Slack APIもGoogle Apps Scriptも不慣れでしたが、社内I/O Dayを活用することで、コードリーディングからブログ投稿まで1日で終えられました。
次の四半期でも、また何か取り組めたらと思っています。
追記 (2024-05-16)
HTTPメソッドを明示的に指定するようコードを変更しました。記事公開時には method: "post"
などの指定をしていませんでした。
瀬良さん、フィードバックありがとうございました!
ありがとうございます!一応どの HTTP メソッドでも動くんですが POST の方が安全かと思うので options に method: "post", を追加しておいた方がよいと思います。あと、この記事からリンクさせていただきました! https://t.co/1QizWfF65i #SlakDevJP
— Kazuhiro Sera (瀬良) (@seratch_ja) 2024年5月15日
*1:記事内のコードはすべてMITライセンスです