Help us understand the problem. What is going on with this article?

RubyでTwitter APIを利用してダイレクトメッセージを送信する

More than 1 year has passed since last update.

sferik/twitter (6.2.0)で自分のアカウントからダイレクトメッセージ送信を試してみようとしたものの、 Twitter::Error::NotFound (Sorry, that page does not exist.) などと怒られて進めなかったため、自分でAPIを直に叩く実装をした。

環境

  • Ruby 2.5.1
  • Rails 5.2.0

成果物

事前準備

  • TwitterでTwitterアカウントを取得する。
  • Twitter Appsでアプリを作成してトークン類を取得する。

注意: トークンのAccess Levelを「Read, write, and direct messages」に設定した後にAccess Tokenを再発行しないとダイレクトメッセージ操作の権限がトークンに付与されない。

MatchLab___Twitter_Application_Management.png

コード

app/services/twitter_service.rb
class TwitterService
  TWITTER_API_DOMAIN          = "https://api.twitter.com"
  TWITTER_CREATE_DM_ENDPOINT  = "#{TWITTER_API_DOMAIN}/1.1/direct_messages/events/new.json"
  TWITTER_SIGNATURE_METHOD    = "HMAC-SHA1"
  TWITTER_OAUTH_VERSION       = "1.0"
  TWITTER_CONSUMER_KEY        = ENV.fetch("TWITTER_CONSUMER_KEY")
  TWITTER_CONSUMER_SECRET     = ENV.fetch("TWITTER_CONSUMER_SECRET")
  TWITTER_ACCESS_TOKEN        = ENV.fetch("TWITTER_ACCESS_TOKEN")
  TWITTER_ACCESS_TOKEN_SECRET = ENV.fetch("TWITTER_ACCESS_TOKEN_SECRET")

  def initialize(user_id, text)
    @user_id = user_id
    @text = text
  end

  def call
    uri = URI.parse(TWITTER_CREATE_DM_ENDPOINT)

    request = Net::HTTP::Post.new(uri)
    request.content_type = "application/json"
    request.body = JSON.generate(request_body_hash)

    request["Authorization"] = authorization_value

    options = { use_ssl: true }

    response = Net::HTTP.start(uri.hostname, uri.port, options) do |http|
      http.request(request)
    end
  end

  private
  def request_body_hash
    {
      event: {
        type: "message_create",
        message_create: {
          target: { recipient_id: @user_id },
          message_data: { text: @text }
        }
      }
    }
  end

  def authorization_value
    authorization_params = signature_params.merge(
      oauth_signature: generate_signature("POST", TWITTER_CREATE_DM_ENDPOINT)
    )
    return "OAuth " + authorization_params.sort.to_h.map{|k, v| "#{k}=\"#{v}\"" }.join(",")
  end

  def signature_params
    @signature_params ||= {
      oauth_consumer_key: TWITTER_CONSUMER_KEY,
      oauth_nonce: SecureRandom.uuid,
      oauth_signature_method: TWITTER_SIGNATURE_METHOD,
      oauth_timestamp: Time.zone.now.to_i,
      oauth_token: TWITTER_ACCESS_TOKEN,
      oauth_version: TWITTER_OAUTH_VERSION
    }
  end

  def oauth_values
    values = signature_params.sort.to_h.map {|k, v| "#{k}=#{v}" }.join("&")
    ERB::Util.url_encode(values)
  end

  def generate_signature(method, url)
    signature_data = [method, ERB::Util.url_encode(url), oauth_values].join("&")
    signature_key = "#{ERB::Util.url_encode(TWITTER_CONSUMER_SECRET)}&#{ERB::Util.url_encode(TWITTER_ACCESS_TOKEN_SECRET)}"
    signature_binary = OpenSSL::HMAC.digest(OpenSSL::Digest::SHA1.new, signature_key, signature_data)
    ERB::Util.url_encode(Base64.strict_encode64(signature_binary))
  end
end

要点

リクエストボディ

Twitter API経由でDirect Messageを送信するためには、Twitter社が Event Object と呼ぶ形式のJSONを送信する必要がある。それがこの部分。
注意: recipient_id にはメンション等に使うアカウント名 (screen_name) ではなく、Twitter側で管理されている user_id を用いる必要がある。 お手軽に調べるなら Twitter IDチェッカー が便利。

  def request_body_hash
    {
      event: {
        type: "message_create",
        message_create: {
          target: { recipient_id: @user_id },
          message_data: { text: @text }
        }
      }
    }
  end
key value
event.type "message_create" 固定
target.recipient_id メッセージの宛先ユーザーID
message_data.text メッセージ内容

何やら別APIで画像や動画をアップロードしてから media: { attachment: { type: "media", media: { id: @media_id } } } のようなプロパティを追加すると、ファイルを添付してメッセージを送信できるようです。 (参考)

Authorizationヘッダ

Authorizationヘッダには OAuth xxx=aaa,yyy=bbb,zzz=ccc みたいな文字列を入れる必要がある。各パラメータについては下記の通り。

key value
oauth_consumer_key Twitter Appsで取得したConsumer Key (API Key)
oauth_nonce APIリクエストごとにユニークな文字列。
oauth_signature_method 署名の形式。 "HMAC-SHA1" 固定。
oauth_timestamp タイムスタンプ(数字)。
oauth_token Twitter Appsで取得したAccess Token。
oauth_version OAuth認証形式のバージョン。 "1.0" 固定。
oauth_signature 認証用の署名(詳細は後述)。Base64エンコードされた文字列。

署名(signature)

この 署名 が最大の難所だったので、公式に則って順を追って説明していく。

必要な手順

認証用情報を生成

Authorizationヘッダに含める情報のうち、 oauth_signature を除く6つのパラメータをキーの名前順に並べて "&" で結合し、さらにURLエンコードした文字列を用意する。それがこの部分。

def oauth_values
  values = signature_params.sort.to_h.map {|k, v| "#{k}=#{v}" }.join("&")
  ERB::Util.url_encode(values)
end
署名用の文字列を生成

リクエストメソッド(今回は "POST" )、リクエストURL(クエリパラメータ無し、1回エンコード)、そして先ほど生成した認証用情報の3つを "&" で結合する。それが def generate_signature 内のこの部分。

signature_data = [method, ERB::Util.url_encode(url), oauth_values].join("&")
Signing key計算用の鍵を生成

Twitter Appsで取得したAccess Token SecretとConsumer Secret (API Secret)をそれぞれURLエンコードしてから "&" で結合する。それがこの部分。

signature_key = "#{ERB::Util.url_encode(TWITTER_CONSUMER_SECRET)}&#{ERB::Util.url_encode(TWITTER_ACCESS_TOKEN_SECRET)}"
Signing keyを生成
signature_binary = OpenSSL::HMAC.digest(OpenSSL::Digest::SHA1.new, signature_key, signature_data)
signatureを生成

最後にSigning keyをBase64エンコードした上でもう一度URLエンコードすると signature が完成です。それがこの部分。

ERB::Util.url_encode(Base64.strict_encode64(signature_binary))

ポイント

  • URLエンコード時は ERB::Util.url_encode を用いる。 (参考)
  • 今回は関係ないが、 GETリクエスト時にクエリパラメータを含む場合は def oauth_values 内で signature_params に加えてクエリパラメータも一緒にソート・結合・エンコードの処理を行う必要がある。
  • Content-Type: application/x-www-form-urlencoded の場合も、リクエストパラメータを上記のように追加してソート・結合・エンコードを行う必要がある。

実行

こういう風に書けば実行できる。

TwitterService.new(user_id, text).call

Could not authenticate you みたいなエラーが返ってきたら、アクセストークン類の再発行とか試してみてください。

余談(背景)

なぜこんな実装が必要になったのかというと、Twitterの自分のアカウントからダイレクトメッセージを送信する機能を実装しようとして、sferik/twitter (6.2.0) を使って実装してみたところエラーになったから。

client = Twitter::REST::Client.new do |config|
  config.consumer_key        = TWITTER_CONSUMER_KEY
  config.consumer_secret     = TWITTER_CONSUMER_SECRET
  config.access_token        = TWITTER_ACCESS_TOKEN
  config.access_token_secret = TWITTER_ACCESS_TOKEN_SECRET
end

client.create_direct_message(user_id, message)
Twitter::Error::NotFound (Sorry, that page does not exist.)

page does not exist って何だと思ってgemの中のコードを読んでみると、こんなコードを見つけた。

ruby/2.5.0/gems/twitter-6.2.0/lib/twitter/rest/direct_messages.rb
def create_direct_message(user, text, options = {})
  options = options.dup
  merge_user!(options, user)
  options[:text] = text
  perform_post_with_object('/1.1/direct_messages/new.json', options, Twitter::DirectMessage)
end

Twitter公式によると、正しいエンドポイントは direct_messages/events/new らしいので、試しに上記コードのパス部分を変更してみても、 Twitter::Error::Forbidden () とか言われる。
実装したかったのはダイレクトメッセージ送信機能だけだったので、ライブラリなしでTwitter APIを直に叩こうと思い立ったことが今回の実装の背景になります。

余談

Authorizationヘッダに入れる文字列を簡単に生成できる laserlemon/simple_oauth というgemを発見してしまったので、こっちを使った方が楽だったかもしれない。
sferik/twitterの中でAuthorizationヘッダ生成用に使われていた)

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away