エンジニアのソフトウェア的愛情

または私は如何にして心配するのを止めてプログラムを・愛する・ようになったか

Discord Webhook で Ruby からファイルを POST する

昨年の 10 月に Discord Webhook で Elixir からファイルを POST する方法を記事に書きましたが、その Ruby 版です。

テキストをPOSTする

テキストのみの場合、net/httpNet::HTTP.post_form メソッドを使うことで簡単に POST できます。 パラメータとして、本文は content 、ユーザ名は username をキーにした Hash を渡します。 このとき content は必須です。

そのほかのフィールドは Discord のドキュメントを参照してください。

ここでは Webhook の URL は次のように環境変数 WEBHOOK_URL に設定してあることにします。

$ export WEBHOOK_URL=https://discord.com/api/webhooks/****/*******

あとは Webhook の URL 、JSON エンコードした body 、ヘッダにコンテントタイプを指定して POST すれば OK です。

require 'net/http'

Net::HTTP.post_form(
  URI.parse(ENV['WEBHOOK_URL']),
  {
    username: 'emattsan',
    content: 'Hello'
  }
)

ファイルを POST する

前回の Elixir の記事でも書きましたが、ファイルをアップロードするときは JSON でなくて multipart/form-data で POST する必要があります。

multipart-post gem を使う

ありがたいことに multipart を扱うための gem が公開されています。

multipart で POST するためのクラス Net::HTTP::Post::Multipart と、ファイルを扱うためのヘルパクラス UploadIO を使います。

まず POST したいファイルから UploadIO のオブジェクトを作成します。

次に New::HTTP::Post::Multipart のオブジェクトを作成します。 ここでドキュメントにあるように、パラメータ の Hash にキーを file にして UploadIO のオブジェクトを渡します。

あとは Net::HTTP::Post を利用するばあいと同じ手順で POST できます。

require 'json'
require 'net/http'
require 'net/http/post/multipart'

url = URI.parse(ENV['WEBHOOK_URL'])

file =
  UploadIO.new(
    File.new('./images/image.jpg'), # POST するファイル
    'image/jpeg',                   # ファイルの Content-Type
    'image.jpg'                     # POST されたときのファイル名
  )

req =
  Net::HTTP::Post::Multipart.new(
    url.path,
    username: 'emattsan',
    content: 'Hello',
    file: file
  )

http = Net::HTTP.new(url.host, url.port)

http.use_ssl = true

http.request(req)

自力で multipart を書く

有用な gem があるのでわざわざ自力で書かなくてもよいのですが、multipart の理解のため、また万が一公開されている gem を利用できないケースのため、一例を上げておきます。

multipart ではバウンダリで区切られた複数のコンテンツを POST の body に記述します。

えー…、詳しくは公式の資料とか Multipart messages - MIME - Wikipedia とか参照してみてください。

require 'net/http'

boundary = 'DiscordMultipartMessage'

url = URI.parse(ENV['WEBHOOK_URL'])

req =
  Net::HTTP::Post.new(
    url.path,
    'Content-Type': "multipart/form-data; boundary=#{boundary}"
  )

# body にバウンダリで区切ったフィールドを記述する
# このときファイルは読み込んだファイルの内容を挿入します
req.body = <<~BODY
--#{boundary}
Content-Disposition: form-data; name="username"

emattsan

--#{boundary}
Content-Disposition: form-data; name="content"

Hello

--#{boundary}
Content-Disposition: form-data; name="file"; filename="image.jpg"

#{File.read('./images/image.jpg')}

--#{boundary}--
BODY

http = Net::HTTP.new(url.host, url.port)

http.use_ssl = true

http.request(req)

いつか読むはずっと読まない:組合せ数学

組合せ論って面白いですよね。 読んでいるといつの間にか「どう書く」の問題を考えてしまっています。