シェルライクなTwitterクライアントをざっくり作ってみた
昨日Twitterでシェルっぽい感じのTwitterクライアントあったらおもしろくない? とか言っていて, なんか楽しそうだったのでざっくり作ってみました.
使い方はこんな感じ.
$ ruby twsh.rb -u your_twitter_id -p password twsh% echo "はろー twsh.rb" twsh% ls screen_name1: tweet1 screen_name2: tweet2 screen_name3: tweet3 screen_name4: tweet4 screen_name5: tweet5 twsh% exit $
下記コードをtwsh.rbとかで保存して, Rubyとして実行してあげれば起動します. -pオプションはパスワードで必須ですが, -uによるTwitter IDは省略すると環境変数USERが使われます.
後はechoで発言, lsでTL取得です. exit(quit, logout)でシェルを抜けます*1.
ちなみにTwsh.rbとして実装されていないコマンドを用いると, 普段使っているシェルに実行を委ねます. そしてバッククォートによるコマンドの実行を備えているので:
twsh% echo "ボクの$ unameは" `uname` です
とかやると, "ボクの$ unameは Darwin です"って出せたり.
適当で抜けとかありますが, 興味がある方は適当にいじっちゃってくだしあ.
あ, あとボクがTwitterでぶつくさ言っていたら@omasanoriさんが興味を持ったようでして, PythonでTwsh.pyを作っているみたいです! たぶんTwsh.rbよりクールなやつができあがると思いますよ!
require 'optparse' require 'stringio' require 'readline' require 'rubygems' require 'twitter' module Twsh class Logout < StandardError; end class Shell class << self def login(argv, env) new argv, env end end def initialize(argv, env) username = env['USER'] password = nil OptionParser.new do |opt| opt.on('-u VAL') {|v| username = v} opt.on('-p VAL') {|v| password = v} opt.parse! argv end fail if username.nil? || password.nil? @client = Twitter::Base.new Twitter::HTTPAuth.new username, password input = Input.new self logged_in = true while logged_in begin output = execute input.read puts output if output rescue Logout logged_in = false rescue Interrupt puts logged_in = false end end end attr_reader :client def execute(argv) return if argv.empty? command = argv.shift Command.new(self).execute command, argv end end class Command @@commands = {} class << self def setup fail unless block_given? yield new end end def initialize(twsh = nil) @twsh = twsh end def on(*commands, &process) fail unless block_given? commands.each {|command| @@commands[command] = process} end def execute(command, argv) process = @@commands[command] if process args = [] args << @twsh if process.arity >= 1 args << argv if process.arity >= 2 output = nil StringIO.open '', 'w' do |io| orig = $stdout $stdout = io process.call *args $stdout = orig output = io.string unless io.string.empty? end output else `#{command} #{argv.join ' '}`.chomp end end end class Input def initialize(twsh) @twsh = twsh end def read read_with_context end private def read_with_context(context = nil, argv = []) prompt = context ? "#{context}> " : argv.empty? ? "twsh% " : '> ' input = Readline.readline prompt, true context, argv, continue = parse "#{input}\n", context, argv continue ? read_with_context(context, argv) : argv end def parse(input, context = nil, argv = []) escape = false continue = false v = !argv.empty? ? argv.pop : '' new = lambda do argv << v unless v.empty? v = '' end input.each_byte do |c| if escape escape = false case c when LINE_FEED continue = true v << c if context when ?n v << "\n" if quoted? context when ?t v << "\t" if quoted? context when ?f v << "\f" if quoted? context when ?v v << "\v" if quoted? context else v << c end else case c when ?\n v << c if context when ?\s if context v << c else new.call end when ?' if context == :quote new.call context = nil elsif context v << c else context = :quote end when ?" if context == :dquote new.call context = nil elsif context v << c else context = :dquote end when ?` if context == :bquote v = @twsh.execute(parse(v)[1]) context = nil elsif context v << c else context = :bquote end when ?\\ escape = true else v << c end end end new.call [context, argv, continue || !context.nil?] end def quoted?(context) context == :quote || context == :dquote end end end Twsh::Command.setup do |c| c.on('exit', 'quit', 'logout') { raise Twsh::Logout } c.on 'echo' do |twsh, argv| twsh.client.update argv.join ' ' end c.on 'ls' do |twsh, argv| n = 5 OptionParser.new do |opt| opt.on('-n VAL', Integer) {|v| n = v} opt.parse! argv end twsh.client.home_timeline(:count => n).each do |status| puts "#{status.user.screen_name}: #{status.text}" end end c.on 'cd' do |twsh, argv| if argv.empty? Dir.chdir else Dir.chdir File.expand_path argv.first end end end Twsh::Shell.login ARGV, ENV
*1:Ctrl + cでもいいんですが, Readlineが捉えちゃっているのかInterruptがうまく投げられない……
名前空間つきのモジュールを使う
いつからこうなっているか知らないんですけど, Erlangって名前空間があるらしいです.
-module(ffhh.followers.director).
こんな感じでモジュールを任意の名前空間に属させるコトができます. アンダースコアで長ったらしい名前をつけなくてもいいワケですね! 例えば呼び出しとかも, 同一の名前空間に属していれば同一部分は省略できます. こんな感じ.
-module(ns.mod1). -export([fun1/0]). fun1() -> Result = mod2:fun1(), % 実際にはns.mod2:fun1/0がコールされている .lists:reverse(Result). % ただ存在しないモジュールはルートから検索してくれるワケではないので, % 先頭にドットをつけて, 明示的にルートから検索するようにしないとダメ % (ちょっとめんどくさい)
ステキ! 少しめんどうなところもあるし, 実際OTP使ったりすると, どこから呼ばれるのかわからないので, 先頭にドットつけてフルパスで指定した方が結局無難じゃん! とかありますけども, まぁそれは些細な話というコトで.
んで名前空間を使うと, ディレクトリの構造も変えなくちゃいけなくて, CPANよろしくな構造にしないといけないんですよね.
コレを:
src ├── ns.erl └── ns_mod1.erl
こんな風に:
src ├── ns.erl ├── ns └── mod1.erl(モジュール名はns.mod1)
あと*.beamファイルの出力先も同じような構造になってなくちゃいけません. *.erlと同じところに入れるなら簡単ですが, ebinなど別のディレクトリに出力するならディレクトリを作りつつコンパイルしなくちゃいけないはず.
というコトでそれを行うRakefileを書いてみた*1. MakefileじゃないのはただボクがMakefileの書き方をよく知らないってだけです.
require 'rake/clean' BEAM_DIR = 'ebin' def dirname(files) (files.map {|f| File.dirname f}.uniq - [BEAM_DIR]).sort_by {|f| f.length} end BEAM_OBJ = FileList['src/**/*.erl'].sub(/^src/, BEAM_DIR).ext 'beam' BEAM_DIRS = dirname BEAM_OBJ CLOBBER.include BEAM_OBJ + BEAM_DIRS BEAM_DIRS.each {|dir| directory dir} def modules(*mods) files = mods.flatten.map do |mod| segs = mod.split '.' segs[-1] = "#{segs.last}.beam" File.join BEAM_DIR, segs end dirname(files) + files end def resolve(ext) lambda {|obj| obj.sub(/^#{BEAM_DIR}/, 'src').sub(/\.[^.]+$/, ".#{ext}")} end ERLC = 'erlc' ERLC_FLAGS = '-W -Iinclude' task :default => modules('ns', 'ns.mod1') rule /\.beam$/ => resolve('erl') do |t| sh "#{ERLC} #{ERLC_FLAGS} -o #{File.dirname t.to_s} #{t.source}" end
楽ちんだぜー!
*1:ebinは最初からある想定です. *.appを置かない場合はebinも作るようにした方がいいと思います
新しいふぇらほい forefrontの構造
単なる走り書きです.
(やべぇ, 透過しっぱなしだ)
灰色の四角形は全部モジュールで, 太めの字はモジュール名, その下に書かれた小さな字は使用しているビヘイビアの名前です(存在しないのはビヘイビアを使っていない). 水色で囲われたところはsupervision tree. ffhh_supの下に並ぶモジュールはsupervisorの子となります.
ffhh
各プロセスへのインタフェースを備えているだけ. このモジュールから各プロセスに指示を出す.
ffhh_app
アプリケーション. ただsupervision treeを作るよう指示するだけ. まじ中身空っぽ.
ffhh_sup
スーパバイザ. どんなsupervision treeを作るかを定義しているだけなので, ffhh_app並みに内容がない.
ffhh_logger
ロガー. さまざまなプロセスからのログ出力要求に応えてログを吐くだけ.
ffhh_director
ユーザーを保持しているディレクタ. こいつがタイミングを見計らってクローラの生成/回収を行う.
ffhh_crawler
指定されたユーザーのフォロワーを捜査するクローラ. スーパバイザの管理下ではないので注意. ただクローラの死亡はディレクタがきっちり面倒みてる.
ffhh_judgement
ユーザーがスパムかそうでないかを判断する. クローラはこいつによってフォロワーをスパム or スパムでない, に振り分ける.
ffhh_sweeper
アカウントをスパムとして報告またはブロックする. クローラによって集めたスパムアカウントをこいつによってほいほいする. ほいほいした情報も蓄えている.
Twitter Streaming APIをErlangから使ってみる
TwitterさんはたくさんAPIを公開しており, 開発者からしたら頭が下がる想いですが, その中でもちょっと特殊なStreaming API.
Twitter API Wiki / Streaming API Documentation
Public Timeline上を流れるつぶやきを次々と受け取るAPIですね. 種類があるのでとりあえず書いておきます.
メソッドの種類
sampleメソッド
Public Timeline上のつぶやきをランダムにピックアップし, 応答として返してくれるメソッドです. 「とりあえずどんな感じなのか知りたいんだけど」って人にもってこいです.
- URL
- http://stream.twitter.com/1/statuses/sample.format
- リクエストメソッド
- GET
firehoseメソッド
Public Timeline上の全つぶやきを応答として返してくれるメソッド. おそらく消化ホース(firehose)の中を流れる水のよう, として名付けられたんじゃないでしょうか? 全つぶやきなのでそれはそれは激流なんだろうなぁ, と勝手に想像してました.
しかしこのメソッド, 利用時のアカウントに特定のロール(権限)が与えられていないと使えません. なので普段はあまり使(わ|え)ないでしょう.
- URL
- http://stream.twitter.com/1/statuses/firehose.format
- リクエストメソッド
- GET
filterメソッド
使うならこっちのfilterメソッド. Public Timeline上から条件に該当するつぶやきを応答として返してくれるメソッドです. 指定できる条件はユーザーID(follow)または任意のキーワード(track), です. 双方ともカンマ区切りで複数指定できます. 片方, または両方指定するコトもできますが, 両方省略するコトはできません. firehoseメソッドと同じになっちゃいますからね.
注意するコトはfollowにはScreen nameではなく, ユーザーIDしか指定できません. ボクだったらtakkkunではなく, 14976270です. またtrackにはマルチバイト文字を指定するコトができないようです. またデフォルトではfollowに200個, trackに400個までしか指定できません.
- URL
- http://stream.twitter.com/1/statuses/filter.format
- リクエストメソッド
- POST
retweetメソッド
コレだけ試していないのでなんともですが, Public Timeline上を流れるRetweetsを応答として返すメソッドでしょう. そんな感じ.
- URL
- http://stream.twitter.com/1/statuses/retweet.format
- リクエストメソッド
- GET
気になるところ
@tsupo さんによるTwitter APIの日本語訳があります.
twitterAPI.txt at master from tsupo's Twitter-API-Specification--written-in-Japanese- - GitHub
ここのStreaming APIの項にgardenhose, birddog, shadowというのがあります. しかし先のStreaming API Documentにはありません. コレらは代わりにロールという位置づけになっており, そのロールが付与されると, filterメソッドのfollowまたはtrackに指定できるIDまたはキーワードの個数が増える, というものです.
どちらにせよロールは設けるのでしょうが, filterメソッドの制限を拡張するのか, 新たなメソッドが使えるようになるのか, どうなるか気になるところです.
Erlangから実際に使ってみる
とりあえず現状で動くコードを書いてみます. 試すだけならcurlを使えばいいんですが, コードから扱ってみたかったのでErlangで試してみました.
-module(twitter_stream). -author("KONDO Takahiro <heartery@gmail.com>"). -export([filter/3, stop/1]). filter(User, Password, Data) -> Url = lists:append(["http://", User, ":", Password, "@stream.twitter.com/1/statuses/filter.json"]), Request = {Url, [], "application/x-www-form-urlencoded", Data}, stream(post, Request, processor()). stop(Streamer) -> send(Streamer, stop), receive {Streamer, stopped} -> stopped end. stream(RequestMethod, Request, Processor) -> spawn(fun() -> Options = [{sync, false}, {stream, self}], case http:request(RequestMethod, Request, [], Options) of {ok, RequestId} -> loop(RequestId, Processor); {error, Reason} -> send(Processor, {error, Reason}) end end). loop(RequestId, Processor) -> receive {http, {RequestId, stream_start, Headers}} -> send(Processor, {start, Headers}), loop(RequestId, Processor); {http, {RequestId, stream, Part}} -> case Part of <<"\r\n">> -> noop; _ -> send(Processor, {stream, Part}) end, loop(RequestId, Processor); {http, {RequestId, {error, Reason}}} -> send(Processor, {error, Reason}); {From, stop} -> http:cancel_request(RequestId), send(From, stopped) end. processor() -> spawn(fun() -> process_loop() end). process_loop() -> receive {_Streamer, {start, Headers}} -> io:format("Start: ~p~n", [Headers]), process_loop(); {_Streamer, {stream, Part}} -> io:format("Stream: ~p~n", [Part]), process_loop(); {_Streamer, {error, Reason}} -> io:format("Error: ~p~n", [Reason]) end. send(To, Message) -> To ! {self(), Message}.
つぶやきひとつひとつは改行で区切られるので, それを元にして切り出しています.
http:request/4のオプションとして{stream, self}を用いると, http:request/4を呼び出したプロセスに次々とレスポンスが流れ込んでくるので, それを改行区切りで別プロセスに渡すだけです. 実用するならその別プロセス(コード上ではprocessor)を外から渡せるようにした方がいいでしょうね.
とりあえずこんな感じに使います. erlang(大文字小文字は関係ない)を含むつぶやきを受け取ります.
% erl Erlang R13B01 (erts-5.7.2) [source] [smp:2:2] [rq:2] [async-threads:0] [hipe] [kernel-poll:false] Eshell V5.7.2 (abort with ^G) 1> inets:start(). ok 2> Streamer = twitter_stream:filter("Twitter ID", "Password", "track=erlang"). <0.48.0> % 標準出力に結果を出力しているので, ここに応答が次々と表示される 3> twitter_stream:stop(Streamer). stopped
コレでStreaming APIからつぶやきを受け取りつついろんなコトができそうです.
Twitterにまつわるいろいろな制限について
こんにちは! みなさん今日もTwitterで元気につぶやいていますか!? さえずっていますか!?
ところでよく聞く"規制"ってあるじゃないですか. なんかあまりにヒマだったのでちょっと調べてみたのです. そしたらこんなページが.
Twitter Support :: Following and Update Limits
Twitterが設けている制限について記述してあるページです. 抜粋してまとめてみました. まとめただけなので, 対策とかではないです. ちなみに○○制限という名前で統一してありますが, 正式な名前ではないのであしからず.
API制限
TwitterにはAPIというモノが設けられておりますが, ある制限を超えると使用できなくなります. それは1時間あたりの使用回数が150を超えた時です*1. この使用回数は特定のタイミングでリセットされます.
ちなみに誤解されやすいかもしれませんが, この使用回数を消費するAPIと消費しないAPIがあります.
大まかに言うと, TimelineやMentions, DirectMessagesなどの取得に関わるAPIは使用回数を消費します. それ以外のつぶやきの投稿や, フォロー/リムーブ, お気に入りの追加/解除などのAPIは使用回数を消費しません. したがって「つぶやきすぎてAPI制限にひっかかっちゃった!」はありえません.
アップデート制限
じゃあなぜつぶやけなくなってしまうのか. それは別の制限があるからです. それがアップデート制限. つぶやくコトをTwitter側ではステータスのアップデートと表現するのでこういう風に呼んでみました.
どうやらつぶやきは1日に1,000件までしか受け付けないようです. Webからつぶやこうが, クライアントからAPI経由でつぶやこうが, 1,000を超すコトはできないみたいです. あとDirectMessageは1日に250件までです.
フォロー制限
あとフォローに関しても制限があります. ただ大した話ではないですが.
簡単に言うとfollowingを極端に増やすコトはできない, という話です. followingとfollowerの比率によって最大following数が決まります.
おかしな400 Bad Requestについて
制限にひっかかると, Twitter側からの応答で400 Bad Requestというのが返ってきます. 「その要求は受け付けられないなぁ」というコトですね. 試しにAPIを150回使用した後で, 追撃のAPIを使用すると400 Bad Requestが返ってきます.
しかしつぶやきが1,000に到達していないはずなのに, つぶやいたときに400 Bad Requestが返ってくるときがあります. ボク自身だと, ここ数日の深夜1時から朝7時ぐらいまでつぶやけないときがあります. それもまったくではなく, 1回2回つぶやくのに成功して, その後またつぶやけないとう具合に.
コレは先のページにも書かれていない現象なので, ただの推測ですがTwitter側の隠れた負荷軽減措置のような気がします. ボクは10,000回つぶやいた後ぐらいからこの影響を初めて受けたので, つぶやいた回数が多い人にこの措置が適用されているのかもしれません.
まとめ
追記[2009-10-12 1:41]
制限に関するページが他にもありました.
Twitter API Wiki / Rate limiting
気がかりなのは一番下のブラックリストに関連するところ. 怪しい挙動のアプリケーション(OAuthコンシューマも含むのでしょうか)は自動的にブラックリスト入りされるとか……?
Twitterのしつこいフェ○アイコンを自動でブロックするサービスを作ったよ
こちらでーす.
名前に関してはいろいろありますが, とりあえずBritney Fuckedのせいにしておいてください. あ, ごめんなさいすみません, あ, あ. 悪ふざけです.
使い方はOAuthの方で許可していただければそれだけでOKです. 詳しいコトはふぇらほいの方に書いてありますけどね.
使ったもの
せっかくなんで書いておきます. 余力があったらソースコードでも公開します.
ハマったところ
Web側は特に問題もないのですが, クローラなどのバックグラウンドで動作するやつを書いたコトがないので, 紆余曲折ありました.
最初の作りがあまく, 200ユーザー数を超えたあたりでとあるプロセスがヒープ領域を大量に消費してしまったり, プロセスの死亡をトラップせず, それが影響して全体が死んだり…… ユーザー数も予想外の増加数だったのでけっこう焦ったー. 公開して1日経たずにバックグラウンドのプログラムを書き換えるはめになりました.
学んだコトは:
- いくらErlangのプロセスが軽いとは言え, そのプロセスに引き連れている値(loop(A, B, C)など)によっては考えモノ
- メモリ的には問題なくても, 他にボトルネックとなる部分がある. 今回はTwitter APIへのリクエスト. 並列数多すぎてタイムアウトが頻発してた
とかでしょうか. 現在は並列数を最大でも20程度に抑え, プロセスも逐次spawn_linkするようにしてあります.
とりあえず
うっとうしくて, でも無視できなくて, 毎回手動で対処しているという方は一度どうでしょうかー.
くるくる回して入力するUIを作ってみた
とある範囲の値を入力してもらいたいけど, テキストボックスじゃ自由度高すぎるし, 範囲が微妙に広いからコンボボックスじゃ選ぶのめんどいなー*1.
そんなコトを考えているときに「こんなのどうよ?」と思って作ってみた.
とりあえずこんな風になった
このページを開いて, 青いボックスの上でマウスのボタンを押しっぱなしにすると, ちっこい矢印がでてくる. その状態で, その矢印を中心に:
- 時計回りにドラッグするとカウントアップ
- 半時計回りにドラッグするとカウントダウン
します.
とりあえずRackのミドルウェアとして実装すればいいんじゃない?
最近Rackの上にごくごく簡単なフレームワークをSinatraも使わず組んでいるんですが, ミドルウェアが便利で, 仕組みもごくごく単純でステキだなぁとか思っているからとりあえずメモしておこうと思って久しぶりにブログを書こうとふにゃらららら.
仕組みから言えばフィルターとして動きます. たとえば:
use Rack::ShowExceptions use Rack::Lint run ExampleApp.new
ってミドルウェアを積むと, Rack::ShowExceptions#call(env) -> Rack::Lint#call(env) -> ExampleApp#call(env)と順番に呼ばれます. ただチェインしているだけですね. 素直な実装でボクうれしい.
つーコトでオプション的な機能は引数のoptionsとか設けて対応するんじゃなくて, Rackのミドルウェアに任せれば済むんじゃないか. 素直な実装というコトで, 簡単に作れました. 試しに行頭と行末のホワイトスペースおよび改行を削除するミドルウェアを書いてみた.
require 'rubygems' require 'rack' class Strip def initialize(app) @app = app end def call(env) status, headers, body = @app.call env body, content_length = strip body headers['Content-Length'] = content_length.to_s [status, headers, body] end def strip(body) content_length = 0 body.each do |s| s.gsub! /^\s+|\s+$|[\r\n]/m, '' content_length += Rack::Utils.bytesize s end [body, content_length] end end
あとはuse StripってしてあげればOK. 簡単だね.
ただミドルウェアを積む順番を意識するのはめんどうなので, @app.call(env)を必ず呼び出して, その結果を加工する程度のモノにした方がよさそうです. 試しにキャッシュ機能とか実装してみたんですけど, キャッシュがあった時点で後方のアプリケーションを呼び出さないので, 順番によって結果が変わる可能性があるからです.
Rubyで実践 OAuth in Twitter
というコトで前回のエントリに引き続き, OAuthです. 今度はコードを書き, OAuthを用いてTwitterのユーザータイムラインを取得してみようと思います.
コードはGitHubの方に置いてみました. 初めてのGitHubわくわくです.
ちなみにターミナルから起動します. GitHubの方にはREADMEすら置いていないのであしからず.
$ ruby examples/twitter.rb
以下手順を追っていきますが, コードを部分ずつにわけて載せています. 全部が見たい人はGitHubの方で見てみてください.
認証トークンを取得する
consumer = OAuth::Consumer.new CONSUMER_KEY, CONSUMER_SECRET, { :signature_method => OAuth::SignatureMethod::HMAC::SHA1 } request_token = consumer.get_token REQUEST_TOKEN_URL puts "Request token: #{request_token.token}" puts "Request token secret: #{request_token.secret}"
まずコンシューマのキーと秘密の文字列でコンシューマオブジェクトを生成. Twitterは今のところ, signature_methodとしてHMAC-SHA1しか対応していないので, それも指定.
そしてOAuth::Consumer#get_tokenで認証トークンを取得.
Request token: REQUEST-TOKEN Request token secret: REQUEST-TOKEN-SECRET
ユーザーを認証する
%x{open #{AUTHORIZE_URL % request_token.token}} puts 'Press enter key after allowed.' gets
最初に断っておきますと, openを使っているのでMac OS Xでしか動きません(たぶん). まぁAUTHORIZE_URL % request_token.tokenの結果を出力して, 手動でアクセスしてあげれば問題はないはず.
puts AUTHORIZE_URL % request_token.token puts 'Press enter key after allowed.' gets
このアクセスしたページでユーザーの認証と, リソースへのアクセス許可をもらう. Callback URLで指定したURLに飛ぶがそれは無視して, Enterキーを押す.
アクセストークンを取得する
consumer.token = request_token access_token = consumer.get_token ACCESS_TOKEN_URL puts "Access token: #{access_token.token}" puts "Access token secret: #{access_token.secret}" unless access_token.empty? puts 'Optional values:' access_token.each_pair {|key, value| puts " #{key} => #{value}"} end
さっき取得した認証トークン(request_token)をコンシューマにセット. 再度get_tokenを行い, 今度はアクセストークンを取得. ちなみにTwitterはこのとき付随的な情報として認証されたユーザーのuser_idとscreen_nameも返す. OAuthの仕様書を読んでいないが, こういうコトもできるという認識でOK?
Access token: ACCESS-TOKEN Access token secret: ACCESS-TOKEN-SECRET Optional values: user_id => 14976270 screen_name => takkkun
サービスプロバイダ上のリソースにアクセスする
consumer.token = access_token response = consumer.get 'http://twitter.com/statuses/user_timeline.json' puts response.code puts response.body
取得したアクセストークンをコンシューマにセットし, 任意のリソースにリクエストを送る. 今回はユーザータイムラインを取得.
200 [{"text":"\u3055\u3063\u304d\u304b\u3089\u4e0b\u307e\u3076\u305f\u306b\u76ee\u85ac\u3057\u3066\u308b", ~
ちなみにアクセストークンとシークレットを保存しておけば, 次から認証トークンの取得からアクセストークンの取得のステップを飛ばして, いきなりリソースへのアクセスを行うコトができる.
consumer = OAuth::Consumer.new CONSUMER_KEY, CONSUMER_SECRET, { :signature_method => OAuth::SignatureMethod::HMAC::SHA1, :token => OAuth::Token.new('ACCESS-TOKEN', 'ACCESS-TOKEN-SECRET') } response = consumer.get 'http://twitter.com/statuses/user_timeline.json' puts response.code puts response.body
出力は上記と同じ.
取れました
というコトでOAuthによるリソースの取得ができました. 手順を追うといっても上っ面だけなので, 中で何をして, どんなリクエストを送っているかはoauth.rbでも見ていただければわかると思います.
さすがにいろいろ読んだし, コードも書いたので, 最初の流し読みのときよりもかなり理解が深まったかなー.
OAuthを知る
どうやらOAuthというモノがあるらしい. 仕組みを考えだしたのはTwitterの中の人だとか.
そのOAuthについて理解を深めるために, 粛々とつづってみる. 間違っていたら突っ込んでいただけると嬉しいです.
OAuthの目的
OAuthは認証と認可を目的とする. OpenIDは認証だけなので, そこがOAuthとの違い.
この認可とは, 特定のコンシューマが, 認証されたユーザーの同意によって, サービスプロバイダ上にあるリソースへアクセスできるようになる, というモノ.
OAuthの登場人物
サービスプロバイダさん
ユーザーの認証と, リソースへ対するコンシューマからのアクセスを認可するサービスのコト. OpenIDでいうOpenID Providerの立場.
くだけて言うとOAuthサーバーみたいな感じ. 例えばTwitterとか.
コンシューマさん
リソースへのアクセスを, ユーザーの同意の上で行うサービスのコト. サービスと書いたが, 別にデスクトップアプリでもいい. OpenIDでいうRelying Partyの立場.
くだけて言うとOAuthクライアントみたいな感じ. 例えばTwitter用のデスクトップで動作するクライアントとか.
ユーザーさん
コンシューマから「ちょっとサービスプロバイダ上のリソースにアクセスしたいので, 許可してもらえるかな?」と言われる人.
OAuthの仕組み
リソースへのアクセスは認可もそうだが, まず認証されていなければならないので, たいていIDとパスワードが必要となる. そこをOAuthはトークンと言うランダムな文字列で代用するようにして, リソースへのアクセスを認めている, って感じ. このトークンは2種類ある.
OAuthを使うための準備
まずサービスプロバイダに「こういうコンシューマを作るので, お願いします」という登録が必要. それでコンシューマのキーと秘密の文字列を貰う. 秘密の文字列は署名(Signature)を生成するのに必要で, キー自体は認証トークンをもらうときも, アクセストークンを貰うときも, それ以後のアクセスを行うときも……, すなわちすべてのリクエストに含めなきゃいけない. でないと, どのコンシューマからのアクセスかわからないから.
というわけで, Twitterでコンシューマを登録してみる. まずhttp://twitter.com/oauth_clientsにアクセス.
"Register a new application »"のリンクをクリックして, コンシューマの情報を入力する.
特に重要なのがCallback URL. ユーザーの認証が済んだ後, ここに記入したURLにリダイレクトされるようになっている. なので大体はアクセストークンの取得を行うURLを書くコトになると思う.
そして登録したコンシューマの情報が記載されているページ.
Consumer keyがコンシューマのキー. Consumer secretが秘密の文字列となる. あと認証トークンとアクセストークンを取得するさいに用いるURL(Request token URL, Access token URL)と, ユーザーの認証およびリソースへのアクセス許可をしてもらうためのURL(Authorize URL)も書かれている.
これらの情報でサービスプロバイダとやりとりできる. ひとまずコンシューマの登録は完了したので, 準備はコレでおしまい.
次からコードを書いて実践してみる.