Slackのfiles.getUploadURLExternalでどハマりした


Slackから「files.uploadなくなるよー」って連絡が来たので「API変えればいいんでしょ？よゆーっす。」と思って対応したらえらい大変だったので誰かのお役に立てばと思い投稿しました。

最終的なコードは[正解のコード](https://qiita.com/drafts/54ac70fa3031fa470e40/edit#%E6%AD%A3%E8%A7%A3%E3%81%AE%E3%82%B3%E3%83%BC%E3%83%89)セクションにありますので、細かいことはいいから答えだけ知りたい方はそちらを見てください。

以下、コードはRubyです。

# Gemアップデートすればいいじゃん

そうです。Rubyにもslack-ruby-clientというGemがあるので、こちらのバージョンを上げれば対応出来るのですが、私の環境ではGemの依存関係的にslack-ruby-clientのバージョンを上げることができなかったので、自分でなんとかすることにしました。

# 新しいAPIについて調べた

files.getUploadURLExternalでググれば↓のような記事はたくさん見つかるのですが、いかんせんどれも「URL取得して、URLにファイルをPOSTして、完了するだけ！」としか書かれていません。

https://qiita.com/seratch/items/971980506da92ea6c8db

# 言われた通りにやってみた

とりあえず言われた通りにやってみました。

```
def upload_to_slack(temp_file)
  token = 'Slackのトークン'
  # ステップ1：アップロード先のURLを取得
  params = { token: token, filename: temp_file.original_filename, length: temp_file.tempfile.length }
  res = Net::HTTP.post_form(URI.parse('https://slack.com/api/files.getUploadURLExternal'), params)
  # ステップ2：ファイルをアップロード
  uri = URI.parse(res_json['upload_url'])
  # あれ？どうやってPOSTするんだ？
end
```
そうです。ステップ1は[APIのマニュアル](https://api.slack.com/methods/files.getUploadURLExternal)通りにやればURLが返ってきたのですが、返ってきたURLにどうやってファイルをアップロードするかがどこにも書いていません...
四苦八苦したのですが、公式に聞いたほうが早いと思い問い合わせたところ、

>>Upload a file by making a POST request to the upload URL.
>
>この操作を実施する際にはパラメータを付ける必要はなく、Body 部分にアップロードしたいファイルをつけてリクエストを実行します（Postman で実行した際のスクリーンショットを添付します）。
その後以下のドキュメントに記載の手順 3 [ Call files.completeUploadExternal to finalize the upload. ] の操作を実施すると指定したチャンネルにファイルがアップロードされるという流れになります。

という、とてもわかりやすい解説がスクショ付きで送られてきました。

「Body部分につけて送る」をヒントに色々調べた結果、下記のサイトを元にアップロードすることには成功しました。

https://masamitsu-murase.blogspot.com/2017/01/ruby-nethttp-multipart-post.html

```
def upload_to_slack(temp_file, channel_id, comment, username)
  token = 'Slackのトークン'
  # メッセージを投稿
  chat_res = SlackClient.chat_postMessage(text: comment, username: username, channel: channel_id)
  # ステップ1：アップロード先のURLを取得
  params = { token: token, filename: temp_file.original_filename, length: temp_file.tempfile.length }
  res = Net::HTTP.post_form(URI.parse('https://slack.com/api/files.getUploadURLExternal'), params)
  # ステップ2：ファイルをアップロード
  uri = URI.parse(res_json['upload_url'])
  http = Net::HTTP.new(uri.host, uri.port)
  http.use_ssl = uri.scheme == 'https'
  http.verify_mode = OpenSSL::SSL::VERIFY_NONE
  req = Net::HTTP::Post.new(uri.request_uri)
  fd_attr = [[
    'slack_file', Faraday::UploadIO.new(temp_file.tempfile, temp_file.content_type),
    { filename: temp_file.original_filename, content_type: temp_file.content_type }
  ]]
  req.set_form(fd_attr, 'multipart/form-data')
  http.request(req)
  # ステップ3：処理を完了
  params = { token: token, channel_id: channel_id, thread_ts: chat_res.ts, files: [{'id': res_json['file_id']}] }
  Net::HTTP.post_form(URI.parse('https://slack.com/api/files.completeUploadExternal'), params)
end
```
しかし、これだとファイルのアップロードはエラーにならないのですが、完了の処理が下記のエラーを返すのです。
```
{
  "ok"=>false, "error"=>"invalid_arguments", 
  "response_metadata"=>{
    "messages"=>["[ERROR] must provide an object [json-pointer:/files/0]"
  }
}
```
「いやいや。さすがにマニュアル通りに書いてエラーはないでしょ。」と思いつつ、再度お問い合わせしたり色々試したところ、ついに成功しました！

そのコードがこちらです。

# 正解のコード
```
def upload_to_slack(temp_file, channel_id, comment, username)
  token = 'Slackのトークン'
  # メッセージを投稿
  chat_res = SlackClient.chat_postMessage(text: comment, username: username, channel: channel_id)
  # ステップ1：アップロード先のURLを取得
  params = { token: token, filename: temp_file.original_filename, length: temp_file.tempfile.length }
  res = Net::HTTP.post_form(URI.parse('https://slack.com/api/files.getUploadURLExternal'), params)
  # ステップ2：ファイルをアップロード
  uri = URI.parse(res_json['upload_url'])
  http = Net::HTTP.new(uri.host, uri.port)
  http.use_ssl = uri.scheme == 'https'
  http.verify_mode = OpenSSL::SSL::VERIFY_NONE
  req = Net::HTTP::Post.new(uri.request_uri)
  fd_attr = [[
    'slack_file', Faraday::UploadIO.new(temp_file.tempfile, temp_file.content_type),
    { filename: temp_file.original_filename, content_type: temp_file.content_type }
  ]]
  req.set_form(fd_attr, 'multipart/form-data')
  http.request(req)
  # ステップ3：処理を完了
  params = { token: token, channel_id: channel_id, thread_ts: chat_res.ts, files: "[{'id': '#{res_json['file_id']}'}]" }
  Net::HTTP.post_form(URI.parse('https://slack.com/api/files.completeUploadExternal'), params)
end
```
なんとステップ3で渡すfilesの値がハッシュの配列を文字列化した値でなければいけなかったようです。
マニュアルにも下記のように配列を渡すと明記されていたので、まさかそれを文字列として表現するということになかなか気が付かずかなり苦戦しました。

![スクリーンショット 2024-06-07 9.40.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/118515/21d195b6-8d8b-b947-4023-debbf6cc13d2.png)

以上、誰かのお役に立てば幸いです。