Keep on moving

あんまりまとまってないことを書きますよ

Twitter でOAuth 認証してTweetする on Tornado

6月30日にいきなりtwitterアプリが使えなくなる!?twitterのベーシック認証廃止について | ついーたーTweeter.jpという記事をみて、そういえばTwitter関連のアプリの作ったことないことに気づいて、調べて見ました。
ちょっと作ってみたいものもありまして、ついでなのでTornadoで試してみました。

でOAuthって?

あるサービスの認証情報を利用して、別のサービスの管理する情報を扱えるようにする仕組みのようです。
すみません。まだ私も理解し切れていません。下の説明を読むことをおすすめします。

↓一通りの知識はここから
ゼロから学ぶOAuth:特集|gihyo.jp … 技術評論社

↓図がすごくわかりやすい
OAuthプロトコルの中身をざっくり解説してみるよ - ゆろよろ日記

前準備

OAuthするにはConsumer KeyとConsumer Secretを取得する必要があります。
↓の記事が詳しいので参考にしてください。
Sinatra と OAuth を使って Twitter のタイムラインを取得してみた - まちゅダイアリー(2009-08-18)

Tornadoの場合、ちょっとだけ違うのは以下の通りです。

やり方

Tornadoの場合はauth用のモジュールがありますので、これを使います。↓を参照
サードパーティ認証

コード例
import tornado.auth
import tornado.escape
import tornado.httpserver
import tornado.ioloop
import tornado.options
import tornado.web

from tornado.options import define, options

define("port", default=8888, help="run on the given port", type=int)
define('twitter_consumer_key', default="Twitterで取得するConsumerKey", help="For Twitter Oauth")
define('twitter_consumer_secret', default="Twitterで取得するConsumerSecret", help="For Twitter Oauth")

class Application(tornado.web.Application):
    def __init__(self):
        handlers = [
            (r"/", MainHandler),
            (r"/message/home",HomeTimelineHandler),
            (r"/message/update",UpdateHandler),
            (r"/auth/login", AuthHandler),
            (r"/auth/logout", AuthLogoutHandler),
        ]
        settings = dict(
            twitter_consumer_key = options.twitter_consumer_key,
            twitter_consumer_secret = options.twitter_consumer_secret,
            cookie_secret="32oETzKXQAGaYdkL5gEmGeJJFuYh7EQnp2XdTP1o/Vo=",
            login_url="/auth/login",
            xsrf_cookies=True,
        )
        tornado.web.Application.__init__(self, handlers, **settings)


class BaseHandler(tornado.web.RequestHandler):
    def get_current_user(self):
        user_json = self.get_secure_cookie("user")
        if not user_json: return None
        return tornado.escape.json_decode(user_json)

    def save_current_user(self, user):
        self.set_secure_cookie("user", tornado.escape.json_encode(user))

class MainHandler(BaseHandler, tornado.auth.TwitterMixin):
    @tornado.web.authenticated
    def get(self):
        try:
            name = tornado.escape.xhtml_escape(self.current_user["username"])
            try:
                aaa = self.current_user["access_token"]
            except:
                self.write("access_token don't exist")
            self.write("Hello, " + name)
        except:
            self.write('Please <a href="/auth/login">LogIn</a>')

class HomeTimelineHandler(BaseHandler,tornado.auth.TwitterMixin):
    @tornado.web.authenticated
    @tornado.web.asynchronous
    def get(self):
        self.twitter_request(
            "/statuses/home_timeline",
            access_token=self.current_user["access_token"],
            callback=self.async_callback(self._on_get))

    def _on_get(self,tweets):
        if not tweets:
            tweets = []
        for tweet in tweets:
            self.write(tweet['user']["screen_name"] + ":" + tweet["text"] + "<br />")
        self.finish()

class UpdateHandler(BaseHandler,
                  tornado.auth.TwitterMixin):
    @tornado.web.authenticated
    @tornado.web.asynchronous
    def get(self):
        self.twitter_request(
            "/statuses/update",
            post_args={"status": "Testing Tornado Web Server"},
            access_token=self.current_user["access_token"],
            callback=self.async_callback(self._on_post))

    def _on_post(self, new_entry):
        if not new_entry:
            # Call failed; perhaps missing permission?
            self.authorize_redirect()
            return
        self.finish("Posted a message!")

class AuthHandler(BaseHandler, tornado.auth.TwitterMixin):
    @tornado.web.asynchronous
    def get(self):
        if self.get_argument("oauth_token", None):
            self.get_authenticated_user(self.async_callback(self._on_auth))
            return
        self.authenticate_redirect()
    
    def _on_auth(self, user):
        if not user:
            raise tornado.web.HTTPError(500, "Twitter auth failed")
            # Save the user using, e.g., set_secure_cookie()
        self.set_secure_cookie("user", tornado.escape.json_encode(user))
        self.redirect(self.get_argument("next", "/"))

class AuthLogoutHandler(BaseHandler, tornado.auth.TwitterMixin):
    def get(self):
        self.clear_cookie("user")
        self.redirect(self.get_argument("next", "/"))


def main():
    tornado.options.parse_command_line()
    http_server = tornado.httpserver.HTTPServer(Application())
    http_server.listen(options.port)
    tornado.ioloop.IOLoop.instance().start()


if __name__ == "__main__":
    main()

まだよくわかってないこと

OAuthのバージョンについて

TornadoのOAuth認証はOAuthのver1.0にのみ対応してます。
でも近頃のTwitter OAuth関連のBlogを見てるとver1.0a対応してる記事が多いです。
例えば↓

Tornadoでも1.0a対応してる人たちがいて、Notes on making OAuth work with Google - Tornado Web Server | Google Groupsなんて言う話題がMLに流れてます。
Joe Bohmanさんが対応していて現状↓のように対応済みの様子ですね。(ただしリポジトリ版、しかも本家にはまだ取り込まれていません)
404 · GitHub

YoutubeのOAuth対応もされているようで、早く本家に取り込まれることを願ってやみません。
結局サービスによって対応バージョンがまちまちなのかなぁ

ところで

Tornadoのauthでやってるのは↓によると3-legged認証っていうらしい。
tweepyでtwitterの3-legged OAuth認証を試してみた(GoogleAppEngine) « taichino.com
となると、Twittet Botとか作るにはちょっとオーバースペックな認証方法なのかも。

Tornadoでのさらなるコード例

↓のところで、さらに高機能なTornado製のWeb版Twitter Clientが公開されてます。Tornadoらしい書き方がされていてかなり勉強になります。
http://github.com/foremire/TwitTornado/blob/local/twittornado.py

また、Web版Twitter ClientAIM | Chat, Share, ConnectもTornadoとのこと。Brizzlyの制作チームはTornadoにパッチを送ったりしてかなりFW開発に協力しているようです。

まとめ

TornadoはOAuth認証機能を自前で持っていて追加パッケージなしに割合簡単に使うことができます。
この機能は実はgaemaでそのまま利用されています。これを利用するとGoogle App EngineでOAuthが使えるようになります。
kay-framework - A web framework made specifically for Google App Engine - Google Project Hostingでも取り込まれているので是非使ってみるとApp EngineでOAuthを使ったいろいろなサービスが作れるようになると思います。

*1:私はここではまって1日無駄にしましたorz