はじめに
「QualiArts Advent Calendar 2024」の8日目の記事になります。
株式会社QualiArtsでUnityエンジニアをしております、石上です。
私が所属しているプロジェクトでは、開発のやり取りをSlackで行っています。
それに伴い、開発に便利なSlack Botがプロジェクトごとに用意され、活用されています。
一例として、
- ゲームの実機で、現在の画面のスクリーンショットを撮影し、端末のログと共にSlackにアップロードする
があり、スクリーンショットの画像をアップロードする部分でfiles.uploadAPIが使われていました。
しかし、Slackは、2025/03/11 にこのAPIを廃止することを発表しています。

https://api.slack.com/methods/files.upload より引用
そこで今回、私がその移行対応として、
新しいSlack APIを用いたアップロード処理の実装を行ったので、
他のプロジェクトなどでの移行対応の際の参考になればと思い、本記事を執筆することにしました。
今回は、UnityからC#スクリプトを用いてファイルアップロードを行う場合を例に説明します。
APIを用いた、従来のSlackへのファイルアップロード方法
新しい方法を説明するに先立って、従来の方法についても軽く触れておきます。
APIを用いた、従来のSlackへのファイルアップロードは、以下の方法で行えていました。
- 
files.uploadAPI宛に、送りたい画像のバイナリデータと、投稿したい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.getUploadURLExternalAPI宛にリクエストを送って、ファイルのアップロード先のURLとファイルIDを受け取る
- 先ほど受け取ったアップロード先のURL宛に、アップロードしたいファイルのバイナリデータを送信する
- 
files.completeUploadExternalAPI宛に、アップロードしたファイルに関連するファイルIDを送って、アップロードを確定させる
(1)アップロード先のURL・ファイルIDを取得する処理
アップロードしたいファイル名(filename)とファイルのサイズ(length)を指定して、画像のアップロード先のURL(upload_url)とファイルID(file_id)をリクエストで受け取ります。
file_idは後のfiles.completeUploadExternalAPIを呼ぶ際に必要なので、覚えておきます。
アップロード先の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でのファイル投稿の具体的な方法に絞って記載したため、更なる細かい制御の部分には触れておりません。
本記事が皆様の移行対応の参考になれば幸いです。



