はじめに
「QualiArts Advent Calendar 2024」の8日目の記事になります。
株式会社QualiArtsでUnityエンジニアをしております、石上です。
私が所属しているプロジェクトでは、開発のやり取りをSlackで行っています。
それに伴い、開発に便利なSlack Botがプロジェクトごとに用意され、活用されています。
一例として、
- ゲームの実機で、現在の画面のスクリーンショットを撮影し、端末のログと共にSlackにアップロードする
があり、スクリーンショットの画像をアップロードする部分でfiles.upload
APIが使われていました。
しかし、Slackは、2025/03/11 にこのAPIを廃止することを発表しています。
https://api.slack.com/methods/files.upload より引用
そこで今回、私がその移行対応として、
新しいSlack APIを用いたアップロード処理の実装を行ったので、
他のプロジェクトなどでの移行対応の際の参考になればと思い、本記事を執筆することにしました。
今回は、UnityからC#スクリプトを用いてファイルアップロードを行う場合を例に説明します。
APIを用いた、従来のSlackへのファイルアップロード方法
新しい方法を説明するに先立って、従来の方法についても軽く触れておきます。
APIを用いた、従来のSlackへのファイルアップロードは、以下の方法で行えていました。
-
files.upload
API宛に、送りたい画像のバイナリデータと、投稿したいSlackのチャンネルIDやアクセストークンなどを含めてリクエストを送る
以下は、pngファイルをSlackにアップロードするサンプルコードです。
従来のSlackへのファイルアップロードのサンプルコード
//GetBinaryDataは、画像のバイナリデータを取得する独自メソッド
byte[] binaryData = image.GetBinaryData();
string fileName = "uploadFile.png";
string mimeType = "image/png";
var form = new WWWForm();
form.AddField("token", "ここにアクセストークンを書く");
form.AddField("channels", "ここにチャンネル名または、チャンネルIDを書く");
form.AddBinaryData("file", binaryData, fileName, mimeType);
using var request = UnityWebRequest.Post("https://slack.com/api/files.upload", form);
request.SendWebRequest();
新APIでのこれからのアップロード方法
これからは、以下の通り3種類のリクエストを送ってファイルのアップロードを行うことになります。
-
files.getUploadURLExternal
API宛にリクエストを送って、ファイルのアップロード先のURLとファイルIDを受け取る - 先ほど受け取ったアップロード先のURL宛に、アップロードしたいファイルのバイナリデータを送信する
-
files.completeUploadExternal
API宛に、アップロードしたファイルに関連するファイルIDを送って、アップロードを確定させる
(1)アップロード先のURL・ファイルIDを取得する処理
アップロードしたいファイル名(filename
)とファイルのサイズ(length
)を指定して、画像のアップロード先のURL(upload_url
)とファイルID(file_id
)をリクエストで受け取ります。
file_id
は後のfiles.completeUploadExternal
APIを呼ぶ際に必要なので、覚えておきます。
アップロード先のURL・ファイルIDを取得する処理のサンプルコード
using System;
using System.Threading;
using Cysharp.Threading.Tasks;
using Newtonsoft.Json;
using UnityEngine;
using UnityEngine.Networking;
public static class SlackUtility
{
[Serializable]
public class GetUploadUrlExternalResponse
{
[JsonProperty("upload_url")]
public string uploadUrl;
[JsonProperty("file_id")]
public string fileId;
}
private static async UniTask<(string uploadUrl, string fileId)> GetUploadExternalUrlAndFileIdAsync(
string fileName,
byte[] binaryData,
CancellationToken ct
)
{
var form = new WWWForm();
form.AddField("token", "ここにアクセストークンを書く");
form.AddField("filename", fileName);
form.AddField("length", binaryData.Length.ToString());
using var request = UnityWebRequest.Post("https://slack.com/api/files.getUploadURLExternal", form);
var response = await request.SendWebRequest().ToUniTask(cancellationToken: ct);
var parsedResponse = JsonConvert.DeserializeObject<GetUploadUrlExternalResponse>(response.downloadHandler.text);
if (parsedResponse == null)
{
return ("", "");
}
var uploadUrl = parsedResponse.uploadUrl;
var fileId = parsedResponse.fileId;
return (uploadUrl, fileId);
}
}
(2)ファイルをアップロードする処理
(1)で取得したupload_url
宛に、ファイルをアップロードします。
以下のサンプルコードでは、(1)を複数回行い、その後、複数ファイルを並行してアップロードしています。
ファイルをアップロードする処理のサンプルコード
[Serializable]
public class CompleteUploadExternalRequestFile
{
[JsonProperty("id")]
public string fileId;
public string title;
public CompleteUploadExternalRequestFile(string fileId, string title)
{
this.fileId = fileId;
this.title = title;
}
}
/// <summary>
/// 複数ファイルをアップロードできる処理
/// 後のfiles.completeUploadExternalのfilesパラメータに渡すために、
/// アップロードしたファイルのfile_idとtitleをリストにして返す
/// </summary>
private static async UniTask<List<CompleteUploadExternalRequestFile>> UploadFilesAsync(IEnumerable<(string fileName, byte[] binaryData)> uploadFiles, CancellationToken ct)
{
var completeUploadRequestFiles = new List<CompleteUploadExternalRequestFile>();
var uploadFileTasks = new List<UniTask>();
foreach (var uploadFile in uploadFiles)
{
// (1)のサンプルコード内で定義した、アップロード先のURL・ファイルIDを取得する関数を呼ぶ
var uploadUrlAndFileId = await GetUploadExternalUrlAndFileIdAsync(uploadFile.fileName, uploadFile.binaryData, ct);
var uploadUrl = uploadUrlAndFileId.uploadUrl;
var fileId = uploadUrlAndFileId.fileId;
completeUploadRequestFiles.Add(new CompleteUploadExternalRequestFile(fileId, uploadFile.fileName));
// ファイルアップロード処理をタスクに積む
uploadFileTasks.Add(UploadFileAsync(uploadFile.binaryData, uploadUrl));
}
// アップロード処理を並列実行し、全てのアップロード処理が完了するまで待つ
await uploadFileTasks;
return completeUploadRequestFiles;
}
/// <summary>
/// files.getUploadURLExternalのレスポンスとして返ってくるupload_urlに対してファイルをアップロードする
/// </summary>
/// <param name="binaryData">アップロードファイルのバイナリデータ</param>
/// <param name="uploadUrl">アップロード先のURL</param>
private static async UniTask UploadFileAsync(byte[] binaryData, string uploadUrl)
{
var form = new WWWForm();
form.AddField("token", "ここにアクセストークンを書く");
form.AddBinaryData("file", binaryData);
using var uploadFileRequest = UnityWebRequest.Post(uploadUrl, uploadForm);
await uploadFileRequest.SendWebRequest();
}
(3)ファイルのアップロードを確定させる処理
(2)でアップロードしたファイルのファイルID(file_id
)とタイトル(title
)の組の配列のJSON文字列をfiles
としてリクエストに含めて送り、ファイルのアップロードを確定させます。
このリクエストの際に渡したchannel_id
に合致するチャンネルにファイルが投稿されます。
ファイルのアップロードを確定させる処理のサンプルコード
/// <summary>
/// Slackにファイルを投稿する
/// </summary>
/// <param name="channelId">投稿先のSlackチャンネルID</param>
/// <param name="uploadFiles">アップロードするファイル名とバイナリデータ(複数ファイル指定可)</param>
/// <param name="ct">CancellationToken</param>
public static async UniTask PostUploadMultipleFilesAsync(
string channelId,
IEnumerable<(string fileName, byte[] binaryData)> uploadFiles,
CancellationToken ct = default)
{
var completeUploadRequestFiles = await UploadFilesAsync(uploadFiles, ct);
var uploadFileArrayString = JsonConvert.SerializeObject(completeUploadRequestFiles);
var completeUploadExternalRequestForm = new WWWForm();
completeUploadExternalRequestForm.AddField("token", "ここにアクセストークンを書く");
completeUploadExternalRequestForm.AddField("channel_id", channelId);
completeUploadExternalRequestForm.AddField("files", uploadFileArrayString);
using var completeUploadExternalRequest = UnityWebRequest.Post("https://slack.com/api/files.completeUploadExternal", completeUploadExternalRequestForm);
await completeUploadExternalRequest.SendWebRequest();
}
作成したコード
前項のサンプルコードに加えて、
エラーハンドリングを追加し、整理して完成したコードが以下になります。
このコードは、
- Newtonsoft JSON
- UniTask
に依存しています。
Slack APIでファイルアップロードを行うためサンプルコード(全文)
using System;
using UnityEngine;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using Cysharp.Threading.Tasks;
using UnityEngine.Networking;
using Newtonsoft.Json;
/// <summary>
/// Slack APIを叩くためのUtilityクラス
/// </summary>
public static class SlackUtility
{
/// <summary>
/// Slack APIを叩いた際の例外
/// </summary>
public class SlackException : Exception
{
public SlackException(string message) : base(message)
{
}
}
[Serializable]
private class CompleteUploadExternalRequestFile
{
[JsonProperty("id")] public string fileId;
public string title;
public CompleteUploadExternalRequestFile(string fileId, string title)
{
this.fileId = fileId;
this.title = title;
}
}
public class SlackUploadFile
{
public string FileName { get; }
public byte[] BinaryData { get; }
public SlackUploadFile(string fileName, byte[] binaryData)
{
FileName = fileName;
BinaryData = binaryData;
}
}
private const string AuthToken = "ここにアプリの「Bot User OAuth Token」を書く";
private const string SlackApiUrlPrefix = "https://slack.com/api/{0}";
/// <summary>
/// Slackにファイルを投稿する
/// </summary>
/// <param name="channelId">投稿先のSlackチャンネルID</param>
/// <param name="uploadFiles">アップロードするファイル名とバイナリデータ(複数ファイル指定可)</param>
/// <param name="ct">CancellationToken</param>
public static async UniTask PostUploadMultipleFilesAsync(
string channelId,
IEnumerable<SlackUploadFile> uploadFiles,
CancellationToken ct)
{
var completeUploadRequestFiles = await UploadFilesAsync(uploadFiles, ct);
var uploadFileArrayString = JsonConvert.SerializeObject(completeUploadRequestFiles);
var completeUploadExternalRequestForm = CreateBaseForm();
completeUploadExternalRequestForm.AddField("channel_id", channelId);
completeUploadExternalRequestForm.AddField("files", uploadFileArrayString);
await RequestAsync("files.completeUploadExternal", completeUploadExternalRequestForm, ct);
}
/// <summary>
/// Slackにファイル投稿を行うためのアップロード先のURLと、
/// ファイルのアップロードを完了させるAPIに渡すファイルIDを取得する
/// </summary>
private static async UniTask<(string uploadUrl, string fileId)> GetUploadExternalUrlAndFileIdAsync(
SlackUploadFile uploadFile, CancellationToken ct)
{
var form = CreateBaseForm();
form.AddField("filename", uploadFile.FileName);
form.AddField("length", uploadFile.BinaryData.Length.ToString());
var response = await RequestAsync("files.getUploadURLExternal", form, ct);
var uploadUrl = (string) response["upload_url"];
var fileId = (string) response["file_id"];
return (uploadUrl, fileId);
}
/// <summary>
/// 複数ファイルをアップロードできる処理
/// 後のfiles.completeUploadExternalのfilesパラメータに渡すために、
/// アップロードしたファイルのfile_idとtitle(CompleteUploadExternalRequestFile)をリストにして返す
/// </summary>
private static async UniTask<List<CompleteUploadExternalRequestFile>> UploadFilesAsync(
IEnumerable<SlackUploadFile> uploadFiles, CancellationToken ct)
{
var completeUploadRequestFiles = new List<CompleteUploadExternalRequestFile>();
var uploadFileTasks = new List<UniTask>();
foreach (var uploadFile in uploadFiles)
{
var uploadUrlAndFileId =
await GetUploadExternalUrlAndFileIdAsync(uploadFile, ct);
var uploadUrl = uploadUrlAndFileId.uploadUrl;
var fileId = uploadUrlAndFileId.fileId;
completeUploadRequestFiles.Add(new CompleteUploadExternalRequestFile(fileId, uploadFile.FileName));
// ファイルアップロード処理をタスクに積む
uploadFileTasks.Add(UploadFileAsync(uploadFile.BinaryData, uploadUrl));
}
// アップロード処理を並列実行し、全てのアップロード処理が完了するまで待つ
await uploadFileTasks;
return completeUploadRequestFiles;
}
/// <summary>
/// files.getUploadURLExternalのレスポンスとして返ってくるupload_urlに対してファイルをアップロードする
/// </summary>
private static async UniTask UploadFileAsync(byte[] binaryData, string uploadUrl)
{
var uploadForm = CreateBaseForm();
uploadForm.AddBinaryData("file", binaryData);
using var uploadFileRequest = UnityWebRequest.Post(uploadUrl, uploadForm);
await uploadFileRequest.SendWebRequest();
}
private static WWWForm CreateBaseForm()
{
var form = new WWWForm();
form.AddField("token", AuthToken);
return form;
}
private static async UniTask<Dictionary<string, object>> RequestAsync(
string apiName,
WWWForm form,
CancellationToken ct)
{
var stringBuilder = new StringBuilder();
var requestUrl = stringBuilder.AppendFormat(SlackApiUrlPrefix, apiName).ToString();
using var request = UnityWebRequest.Post(requestUrl, form);
await request.SendWebRequest().ToUniTask(cancellationToken: ct);
var response = JsonConvert.DeserializeObject<Dictionary<string, object>>(request.downloadHandler.text);
HandleError(response);
return response;
}
/// <summary>
/// APIリクエストした際のエラーハンドリング
/// </summary>
private static void HandleError(UnityWebRequest request)
{
if (!string.IsNullOrEmpty(request.error) || request.result != UnityWebRequest.Result.Success)
{
throw new SlackException(request.error);
}
}
/// <summary>
/// APIのレスポンスに対するエラーハンドリング
/// </summary>
private static void HandleError(Dictionary<string, object> response)
{
if (response == null)
{
throw new SlackException("response is null");
}
if (response.TryGetValue("ok", out var ok) && (bool) ok)
{
// API成功
return;
}
if (response.TryGetValue("error", out var error))
{
throw new SlackException((string) error);
}
throw new Exception("unknown error");
}
}
実行例
先ほど作成したコードを利用して、試しに画像を投稿してみます。
1. Slack Appをワークスペースに追加する
まず、files:write
の権限を与えたSlack App(ここでは「slack-utility-test」という名前で作成)を、投稿を行いたいチャンネルのワークスペースへ追加します。
今回は、テスト用に新たにアプリを作っていますが、移行する場合については既にアプリがあると思うので、特にすることはありません(必要に応じて権限を調整してください)。
その後、「OAuth & Permissions」タブのBot User OAuth Tokenを確認し、コピーしておきます。
これを先ほどのソースコード内のAuthToken
として使います。
2. 投稿したいSlackチャンネルへのアプリ追加
投稿したいチャンネルにアプリを追加します。
追加は、Slackアプリをお使いの場合、左下のAppの該当アプリを右クリックして開けるメニューの
「アプリの詳細を表示する」>「チャンネルにこのアプリを追加する」
で行えます。
3. 実行コードの作成
Unityで先ほど作成したコードの
SlackUtility.PostUploadMultipleFilesAsync
関数を呼び出します。
投稿先のチャンネルIDは、チャンネルの詳細ページの下部に記載があります。
using System.Linq;
using System.Threading;
using Cysharp.Threading.Tasks;
using UnityEngine;
public class SlackFileUploadTest : MonoBehaviour
{
private const string ChannelId = "ここに投稿先のチャンネルIDを書く";
[SerializeField] private Texture2D[] textures;
void Start()
{
var uploadFiles =
textures.Select(texture => new SlackUtility.SlackUploadFile("TestImage", texture.EncodeToPNG()));
SlackUtility.PostUploadMultipleFilesAsync(ChannelId, uploadFiles, CancellationToken.None).Forget();
}
}
実行結果
実行すると以下のように、該当チャンネルに、目的の画像が投稿されたことが確認できました。
おわりに
今回は、来年3月に廃止が迫るSlackのfiles.upload
APIについて、移行対応の例をご紹介しました。
本記事では、新APIでのファイル投稿の具体的な方法に絞って記載したため、更なる細かい制御の部分には触れておりません。
本記事が皆様の移行対応の参考になれば幸いです。