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を再発行しないとダイレクトメッセージ操作の権限がトークンに付与されない。
コード
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の中のコードを読んでみると、こんなコードを見つけた。
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ヘッダ生成用に使われていた)