Web上の画像を付けてツイート(1つ・複数)Twitter gemバージョン6.2.0以降の場合

Twitter gem バージョン6.2.0は去る2017年の11/8リリースなのですが、昨日までこの変更に気付いてませんでした(ずっと6.1.0以前を使ってたのさ…)。
さて、以前の記事(Web上の画像を付けてツイート(1つ・複数) - 別館 子子子子子子(ねこのここねこ))で

Twitter::REST::Tweets#update_with_media が楽だと思ったのだが、 API 側が deprecated になった

と書いたのですが、バージョン6.2.0で Twitter::REST::Tweets#uploadが private に変更された(正確には Twitter::REST::Media#upload から Twitter::REST::Tweets#uploadupload メソッドを引っ越して更に private に変更した)ため以前の書き方は使えなくなり*1Twitter gem を使って画像などのメディアをアップするには、API 側に関係なく Twitter::REST::Tweets#update_with_media を使わねばならぬようになりました…。ですので、バージョン6.2.0向けに書き換える必要があります。ぷんすか。

書き方

画像が一つの場合

この場合は img_url 先が画像ではなく動画であってもいけますね、多分。

gem 'twitter', '>= 6.2.0'
require 'twitter'
require 'open-uri'

img = open(img_url)
client.update_with_media(text, img)
画像が複数の場合(例は4画像)
gem 'twitter', '>= 6.2.0'
require 'twitter'
require 'open-uri'

img_urls = [img_url1, img_url2, img_url3, img_url4]
imgs = img_urls.map { |img_url| open(img_url) }
client.update_with_media(text, imgs)
実用例

以前の記事と同様に url_exist? メソッドを使って、画像 URL 先にファイルが本当に存在するか確認しています。また img_urls 配列の中身が5つ以上であった場合には冒頭4つに限定しておきます。

gem 'twitter', '>= 6.2.0'
require 'twitter'
require 'open-uri'

img_urls = [img_url1, img_url2, img_url3,...]
imgs = img_urls[0, 4].map { |img_url| url_exist?(img_url) ? open(img_url) : nil }.compact
client.update_with_media(text, imgs)

twitter_rescue do ブロックを作ってあげれば、より安心です。そして以前の記事の如くメソッドにしてやればラクになりますね。

Twitter::REST::Tweets#update_with_mediaの引数mediaについて

バージョン6.1.0まで
  • media (File, Hash) — A File object with your picture (PNG, JPEG or GIF)

Method: Twitter::REST::Tweets#update_with_media — Documentation for twitter (6.1.0)

画像が1つしか添付出来ない時代の API 向け。なので配列を渡すと

The IO object for media must respond to to_io (Twitter::Error::UnacceptableIO)

と怒られます。

バージョン6.2.0から
  • media (File, Array) — An image file or array of image files (PNG, JPEG or GIF).

Method: Twitter::REST::Tweets#update_with_media — Documentation for twitter (6.2.0)

こちらは配列を渡しても、ちゃんとその中身が IO オブジェクトであるかどうか確認してくれます。

[DEPRECATED] :mime_type option deprecated, use :content_typeと出るのは?

Twitter gem はバージョン6.2.0から http-form_data gem を使っているのですが、この gem のバージョン2.0.0から :mime_type キーが :content_type キーへと変更されました。Twitter gem の private メソッドである Twitter::REST::Request#merge_multipart_file! メソッドで、 HTTP::FormData::File.new のオプションに :mime_type キーが利用されているために
http-form_data gem が [DEPRECATED] を出力しています。
Twitter::REST::Request#merge_multipart_file! メソッドに関する修正ブランチはまだメインブランチに取り込まれていませんが、いずれ取り込まれるのではないでしょうか。

Looks like this option was deprecated here in v1.0.2 of https://github.com/httprb/form_data. The changelog entry is https://github.com/httprb/form_data/commit/5b902fab8d5b6493a400b2f82ac748d8ec0f25d3:title=here].
The changes to this gem would need to be made here.
DEPRECATED :mime_type option deprecated, use :content_type · Issue #881 · sferik/twitter

それでもエラーが出る場合は…もしかして画像ファイルサイズが小さいのかも

画像ファイルサイズが小さいと、Ruby は Tempfile にせずにオンメモリで StringIO のまま処理しようとし、そのために Twitter gem との衝突が生じるようです(バージョン6.1.0以前のときに「Tempfile が保存出来ない」とのエラーが出ていたのはこれが原因だったのかもしれないけど、よくわからない)。

media_url の画像ファイルのサイズが10kb以下と小さい場合に、表題の例外エラーが発生します。
この理由は、ruby の open-uri の open メソッドが、対象ファイルが10kb以下の場合は Tempfile ではなくて StringIO のオブジェクトを返し、gem twitterの update_with_media が to_io メソッドを持たない StringIO オブジェクトを受け付けないため。
twitter gemのupdate_with_mediaで”The IO object for media must respond to to_io”エラー | EasyRamble

対策としては、上に引用した blog や以下の blog のように、StringIO オブジェクトから Tempfile オブジェクトを作ってやることが必要になります。ちょっと面倒ですね。(以下では一旦 open-uri を使ってるけど、上記 blog に類するように Tempfile.open([File.basename(img_url), File.extname(img_url)]) としてに全ての画像 URL 先ファイルを Tempfile にしてしまうやり方でもいいんじゃないかと思ったり。)

  • Solution

... This will handle both the normal and StringIO cases for you by converting StringIO’s to a File.

# lib/twitter/image.rb
module Twitter::Image
  # The Twitter gem is particular about the type of IO object it
  #   recieves when tweeting an image. If an image is < 10kb, Ruby opens it as a
  #   StringIO object. Which is not supported by the Twitter gem/api.
  #
  #   This method ensures we always have a valid IO object for Twitter.
  def self.open_from_url(image_url)
    image_file = open(image_url)
    return image_file unless image_file.is_a?(StringIO)

    file_name = File.basename(image_url)

    temp_file = Tempfile.new(file_name)
    temp_file.binmode
    temp_file.write(image_file.read)
    temp_file.close

    open(temp_file.path)
  end
end

Now, when you’re tweeting an image. You can do this.

image = Twitter::Image.open_from_url(image_url)
twitter_client.update_with_media("Tweet tweet", image)

The IO object for media must respond to to_io

上に引用したblogでは、もう一つの対策として、定数 OpenURI::Buffer::StringMax を強引に書き換える方法も示してあります。open-uri を多用しない*2のであれば、これもアリじゃないかと思います。

OpenURI::Buffer.send :remove_const, 'StringMax' if OpenURI::Buffer.const_defined?('StringMax')
OpenURI::Buffer.const_set 'StringMax', 0

twitter gemのupdate_with_mediaで”The IO object for media must respond to to_io”エラー | EasyRamble

*1:なぜ後方互換性を潰した…

*2:全て Tempfile を作ることになるので、画像ファイルが小さく且つ open-uri を多用している場合には、速度が落ちるおそれがある。