Objective-CでTwitter APIを使う 色々

Twitter APIの認証

Twitter APIの使用は、現在"BASIC認証"と"OAuth"の2通りの方法が用意されている。が、今年6月(?)でBASIC認証が使えなくなるという噂で、今後はAPIを使用するのにはOAuthを使用する必要が出てくるようだ。
まぁBasic認証はパスワードだだ漏れになっちゃうからやめておこうよ、という話ですかね。
Basic認証 - Wikipedia
Code — OAuth

iPhoneアプリTwitter APIを使いたい場合

結構iPhoneTwitterクライアントアプリってたくさんあるけど、どういう実装なのだろう?
大抵は初回起動時に設定画面でユーザー名とパスワードを入力させて、それを使ってBASIC認証でアクセスしているのではないのかな?
BASIC認証を使うAPIアクセスの実装は比較的簡単。(base64エンコーディングを実装せずにBASIC認証を通す方法 - すぎゃーんメモなど)


問題はOAuthを使う場合。
Twitterだと、アプリ作成者は http://twitter.com/oauth_clients でアプリケーションを登録しておき、各ユーザーにこのアプリケーションでAPIアクセスするための許可を得る必要がある。これが面倒。
その許可手続きには通常は2パターンあり、

  • Webアプリの場合:使用許可の確認ページに一度飛ばし、認証+許可されたのちにアプリにリダイレクトしてもらう
  • デスクトップアプリなどの場合:認証用URLを与え、そこでユーザーが認証+許可したあとに表示されるPINコードで認証

どちらかの方法でAPIアクセスに使用するaccess_token, access_token_secretを入手するのだけど、どちらの場合もブラウザを経由する必要がありiPhoneアプリに使用するには非常に手間がかかる。

Objective-C用のOAuthライブラリ

上記の問題に対してはまた後で触れるとして、Objective-CでOAuth使ってアクセスAPIアクセスするにはどうすれば良いか?
組み込みのクラスや関数を使って実装すると非常に大変なので素直にライブラリを使うべき。幾つかあるのだろうけどここでは"OAuthConsumer"を使用する方法を紹介しておく。
Google Code Archive - Long-term storage for Google Code Project Hosting.
チェックアウトしてビルドして、作成されたframeworkを自分のプロジェクトに組み込んで…、と使うようだけど、iPhoneアプリに組み込むには色々と面倒なようだ(たぶんこのへん)。
そこでiPhoneアプリ開発者向けに、必要なファイルだけを集めて整理した、のがこれかな?
GitHub - jdg/oauthconsumer: An iPhone ready, Objective-C implementation of an OAuth consumer.
ここから持ってくるだけでOAuthを使うために必要なものが揃いそう。

$ wget http://github.com/jdg/oauthconsumer/tarball/master
$ tar zxvf jdg-oauthconsumer-0dee9ee.tar.gz

とかで展開しておけばおk。
Xcodeで新規プロジェクトを作成し、Classesに「既存のファイルを追加」で、上記コマンドで展開されたファイル群をまとめて選択、

「デスティネーショングループのフォルダに項目をコピーする」にチェック、「追加したフォルダに再帰的にグループを作成する」を選択。"OAToken_KeychainExtensions.h", "OAToken_KeychainExtensions.h"はあるとビルド失敗してしまうので、除外しておいた方がよさそう。たぶん使わないはず。
これだけやれば、

#import "OAuthConsumer.h"

...

- (void)tweet:(NSString *)message {
    NSURL *url = [NSURL URLWithString:@"http://twitter.com/statuses/update.json"];
    OAConsumer *consumer =
        [[[OAConsumer alloc] initWithKey:@"YOUR-CONSUMER-KEY"
                                  secret:@"YOUR-CONSUMER-SECRET"] autorelease];
    OAToken *accessToken =
        [[[OAToken alloc] initWithKey:@"ACCESS_TOKEN"
                               secret:@"ACCESS_TOKEN_SECRET"] autorelease];
    OAMutableURLRequest *request = [[[OAMutableURLRequest alloc] initWithURL:url
                                                                    consumer:consumer
                                                                       token:accessToken
                                                                       realm:nil
                                                           signatureProvider:nil] autorelease];
    [request setHTTPMethod:@"POST"];
    NSString *bodyString = [NSString stringWithFormat:@"status=%@",
                                     (NSString *)CFURLCreateStringByAddingPercentEscapes(  
                                         kCFAllocatorDefault,
                                         (CFStringRef)message,
                                         NULL,
                                         NULL,
                                         kCFStringEncodingUTF8)];
    [request setHTTPBody:[bodyString dataUsingEncoding:NSUTF8StringEncoding]];

    OADataFetcher *fetcher = [[[OADataFetcher alloc] init] autorelease];
    [fetcher fetchDataWithRequest:request
                         delegate:self
                didFinishSelector:@selector(ticket:didFinishWithData:)
                  didFailSelector:@selector(ticket:didFailWithError:)];
}

- (void)ticket:(OAServiceTicket *)ticket didFinishWithData:(NSData *)data {
    NSString *dataString = [[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] autorelease];
    NSLog(@"data: %@", dataString);
}

- (void)ticket:(OAServiceTicket *)ticket didFailWithError:(NSError *)error {
    NSLog(@"didFailWithError");
}

というコードでTwitterへのOAuthでの投稿ができる。("YOUR-CONSUMER-KEY", "YOUR-CONSUMER-SECRET"は開発者が事前に登録しておく oauth_clients のもの、"ACCESS_TOKEN", "ACCESS_TOKEN_SECRET"が、各ユーザーがそのアプリを認証した際に発行されるもの)
consumer_key, consumer_secretは開発時にアプリに固定で組み込んでしまってもおそらく問題ないと思う(ユーザーには知られないようにするべきだがiPhoneならパケット解析でもされない限り漏れることはない?)。

xAuth

と、上記のようにOAuthでAPIを使用するためには各ユーザーにそのOAuthアプリを認証してもらってaccess_token, access_token_secretを取得する必要があるのだけど、最初の方に書いたとおり、普通の方法だとその認証にはブラウザを経由する必要があり面倒。
そこで出てきた(?)のが"xAuth"というもの。
http://apiwiki.twitter.com/Twitter-REST-API-Method%3A-oauth-access_token-for-xAuth
簡単に言うと、BASIC認証のようにユーザー名とパスワードを送信して認証してもらって、access_token, access_token_secretを取得できるようにする仕組み。
これを使えばブラウザを開く必要なく、OAuthの認証を行うことができる。
注意点として、現在このxAuthを使用するためにはEメールによる使用申請が必要で、それを行わない限りはリクエストは失敗する(上記文書を参照、ググればよくあるメールのテンプレート出てくるのでそれをコピペしておけば3〜4日でOKされると思う)。
基本的にはOAMutableURLRequestでのリクエストと似たようなものだけど、そこに"x_auth_username", "x_auth_password", "x_auth_mode"という3つのパラメータを付加する必要がある。
以下は、xAuthを使ってaccess_token, access_token_secretを取得するためのサンプルコード

- (void)getAccessTokenWithUsername:(NSString *)username password:(NSString *)password {
    NSURL *url = [NSURL URLWithString:@"https://api.twitter.com/oauth/access_token"];
    OAConsumer *consumer =
        [[[OAConsumer alloc] initWithKey:@"YOUR-CONSUMER-KEY"
                                  secret:@"YOUR-CONSUMER-SECRET"] autorelease];
    OAMutableURLRequest *request = [[[OAMutableURLRequest alloc] initWithURL:url
                                                                   consumer:consumer
                                                                      token:nil
                                                                      realm:nil
                                                          signatureProvider:nil] autorelease];

    // 新たに付加するパラメータ
    NSMutableArray *xAuthParameters = [NSMutableArray arrayWithCapacity:3];
    [xAuthParameters addObject:[OARequestParameter requestParameter:@"x_auth_mode" value:@"client_auth"]];
    [xAuthParameters addObject:[OARequestParameter requestParameter:@"x_auth_username" value:username]];
    [xAuthParameters addObject:[OARequestParameter requestParameter:@"x_auth_password" value:password]];

    // 順番が大事!
    [request setHTTPMethod:@"POST"];
    [request setParameters:xAuthParameters];

    OADataFetcher *fetcher = [[[OADataFetcher alloc] init] autorelease];
    [fetcher fetchDataWithRequest:request
                         delegate:self
                didFinishSelector:@selector(ticket:didFinishWithData:)
                  didFailSelector:@selector(ticket:didFailWithError:)];
}

- (void)ticket:(OAServiceTicket *)ticket didFinishWithData:(NSData *)data {
    NSString *dataString = [[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] autorelease];
    // レスポンスの解析
    NSMutableDictionary *dict = [NSMutableDictionary dictionary];
    for (NSString *pair in [dataString componentsSeparatedByString:@"&"]) {
        NSArray *keyValue = [pair componentsSeparatedByString:@"="];
        [dict setObject:[keyValue objectAtIndex:1] forKey:[keyValue objectAtIndex:0]];
    }
    NSLog(@"result: %@", dict);
}

- (void)ticket:(OAServiceTicket *)ticket didFailWithError:(NSError *)error {
    NSLog(@"didFailWithError");
}

これで getAccessTokenWithUsername:password: にユーザー名とパスワードを渡してやると、

result: {
    "oauth_token" = "**************************************************";
    "oauth_token_secret" = "******************************************";
    "screen_name" = sugyan;
    "user_id" = 15081480;
    "x_auth_expires" = 0;
}

という出力が得られて、見事oauth_token, oauth_token_secretを取得することができる。あとはTwiter Clientアプリならこの情報を設定情報に入れておくなどして以降のAPIリクエストで使用するようにすれば良い。

細かい話

上記のようなコードを書くことでOAuthConsumerのライブラリを書き換えることなくxAuthのリクエストに成功したのだけど、見事にハマったところがあったのでメモ。
OAuthConsumerのソースを覗いてみると、xAuthのための3つのパラメータはOAMutableURLRequestの_signatureBaseStringメソッドで作られるSignature Base Stringに付加されなければならなくて、ここに付加するためにはOAMutableURLRequestが内部で持つparametersを追加してやれば良いらしい。これはOAParameterAdditionsというカテゴリがNSMutableURLRequestに追加されているため、プロパティとしてリクエストを変更できるになっている。素晴らしい! …と思いきやこれが罠で、リクエストのmethodがGETの場合このパラメータはURLにQueryStringとして付加されてしまいbodyには含まれなくなってしまう実装になっている。ので、このparametersをsetする前にrequest methodを"POST"に変更しておかなくてはならない。この順番が逆だとOAMutableRequestのデフォルトのmethodは"GET"のため、せっかく追加したparametersがbodyパラメータにならず、正しいrequestを生成できない。

まとまらない

…、とまぁ長々と書いてしまったけど、とにかくそれなりのライブラリを使えばiPhone(Objective-C)でもそれほど難しいことしなくてもOAuthを使用してTwitter APIを叩いたりできるようになる。xAuthだってできる。ということでした。
ライブラリもAPIもまた変化したりするだろうからウォッチし続ける必要はありますが。