7
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

QualiArtsAdvent Calendar 2024

Day 8

Slackのfiles.upload API廃止に伴う、移行対応について(Unity C#の場合を例に)

Last updated at Posted at 2024-12-07

はじめに

QualiArts Advent Calendar 2024」の8日目の記事になります。

株式会社QualiArtsでUnityエンジニアをしております、石上です。
私が所属しているプロジェクトでは、開発のやり取りをSlackで行っています。
それに伴い、開発に便利なSlack Botがプロジェクトごとに用意され、活用されています。

一例として、

  • ゲームの実機で、現在の画面のスクリーンショットを撮影し、端末のログと共にSlackにアップロードする

があり、スクリーンショットの画像をアップロードする部分でfiles.uploadAPIが使われていました。

しかし、Slackは、2025/03/11 にこのAPIを廃止することを発表しています。

files_upload_deprecated.png
https://api.slack.com/methods/files.upload より引用

そこで今回、私がその移行対応として、
新しいSlack APIを用いたアップロード処理の実装を行ったので、
他のプロジェクトなどでの移行対応の際の参考になればと思い、本記事を執筆することにしました。

今回は、UnityからC#スクリプトを用いてファイルアップロードを行う場合を例に説明します。

APIを用いた、従来のSlackへのファイルアップロード方法

新しい方法を説明するに先立って、従来の方法についても軽く触れておきます。
APIを用いた、従来のSlackへのファイルアップロードは、以下の方法で行えていました。

  1. 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種類のリクエストを送ってファイルのアップロードを行うことになります。

  1. files.getUploadURLExternalAPI宛にリクエストを送って、ファイルのアップロード先のURLとファイルIDを受け取る
  2. 先ほど受け取ったアップロード先のURL宛に、アップロードしたいファイルのバイナリデータを送信する
  3. 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として使います。

oauth-tokens_modified.png

2. 投稿したいSlackチャンネルへのアプリ追加

投稿したいチャンネルにアプリを追加します。
追加は、Slackアプリをお使いの場合、左下のAppの該当アプリを右クリックして開けるメニューの
「アプリの詳細を表示する」>「チャンネルにこのアプリを追加する」
で行えます。

app-detail-and-add-to-channel.png

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();
    }
}

slack-channel-id-modified.png

実行結果

実行すると以下のように、該当チャンネルに、目的の画像が投稿されたことが確認できました。
slack-post-result.png

おわりに

今回は、来年3月に廃止が迫るSlackのfiles.upload APIについて、移行対応の例をご紹介しました。
本記事では、新APIでのファイル投稿の具体的な方法に絞って記載したため、更なる細かい制御の部分には触れておりません。
本記事が皆様の移行対応の参考になれば幸いです。

7
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?