Still Life

生活の記録。

OAuthでつぶやく その2 oauthconsumerを使う

もう5ヶ月も前になってしまいましたが、以前iPhoneアプリTwitter連携するためのライブラリについて紹介しました。(OAuthでつぶやく

https://github.com/jdg/oauthconsumer

こちらでダウンロードしたライブラリの取り込みかたについて記事にしたのですが、Twitterと連携できた喜びのあまり勢いだけで記事にしてしまった状態でした。
時間が空いてしまいましたが、フォロー記事を書いてみたいと思います。

結論からいいますと、前回記事に書いた内容の手順はいっさい行わなくても動作しました。
OAuth認証の段階として、oauth_verifierというユーザー情報のひとつを使用するのですが、前回の記事執筆時はまだライブラリに実装されていませんでした。でも最新のライブラリではちゃんと追加されています。ライブラリを持ってくれば、何も難しい手順なくTwitter連携できちゃいます。

これだけではなんなので、ちょっとしたサンプルアプリを作ってみたいと思います。

Twitterへの連携アプリの登録

ブラウザでTwitterを開き、設定>連携アプリ>開発者画面から、アプリ情報の登録を行います。
例では、Application TypeをBrowser、Access type をRead & Writeとします。

Application Typeは、iPhoneアプリなのでClientであるかに思えますが、Browserタイプが直接URLをコールバックしてくれるのに対して、Clientでは単にキーが表示され、ユーザーがそのキーを使って認証を行わないといけないという、あまり親切ではない設計になってしまうのす。

Callback URLはコーディングで使用します。OAuth認証は、最終的にこのURLを、対象ユーザーのアクセスキーをクエリとして返してくれます。URL事体はアプリで認識できればなんでもいいわけですが、やはり自分のアプリのサイトを指定したいものです。
また、URLを取得してからクエリを取れるわけなので、今回作ろうとしているサンプルでは、単純なUIWebViewを使用している以上、どうしても一瞬Callback URLの内容が表示されてしまいます。

アプリの登録が終了しました。
f:id:Aodrey:20110613050924p:image:w320

プロジェクトの作成

Xcodeで新規にView-based Applicationを作成し、OauthconsumerTestという名前で保存しました。

OauthconsumerTestでは、ユーザーにブラウザを介してログイン認証してもらった後、ツイートするというシンプル機能を実装します。
「認証」ボタン、認証時に使用するwebビュー、ツイート内容を入力するテキストフィールド、それに「ツイート」ボタンの備わった画面を作成しておきます。

f:id:Aodrey:20110613015937p:image:w240
こんなかんじです。とにかく1画面で済ませるために、webビューオブジェクトはhiddenオブジェクトを使って必要なときだけ画面前面に出るようにしました。

ライブラリの取り込みの準備

https://github.com/jdg/oauthconsumer
から、ライブラリをダウンロードしてきます。

f:id:Aodrey:20110613002531p:image:w320

ライブラリの取り込み

わたしの場合なのですが、今回のように階層分けされた複数のファイルを一度に取り込むときは、まずは直接プロジェクトフォルダにファイルそのものをコピーし、その後Xcodeから右クリック>追加>既存のファイル...とやったほうがやりやすいです。

f:id:Aodrey:20110613020839p:image:w320
まずは直接ファイルをコピーしてしまう
f:id:Aodrey:20110613020838p:image:w240
プロジェクトに取り込んだ結果

実装

OAuthの認証は、以下の手順で行います。

1.Request token(D)

連携アプリ登録後に取得できるConsumer key(B)、Consumer secret(C)を使用してリクエスト。oauth_token、oauth_token_secretが帰ってきます。

2.Authorize(F)

取得したoauth_tokenをクエリとしてブラウザ表示。ユーザーにログインユーザーIDとパスワードを入力し、ログインしてもらう。正常にアプリが許可されると、コールバックURL(A)が表示されることになるので、アプリ側ではこれを取得する。

3.Access token(E)

コールバックURLに、oauth_token、oauth_verifierが付与されて帰ってくるので、これを使用してリクエスト。

このレスポンスとして、やっとoauth_token、oauth_token_secret、user_id、screen_nameが取得できます。ちなみに、コールバックURLにくっついてきたoauth_tokenと、Access tokenリクエストをして帰って来たoauth_tokenは値が違うので注意です。最終的に取得できたtokenとtoken_secretを使うことで、ツイートなどの各種操作ができます。


実装したコードは以下のとおりです。

OauthconsumerTestViewController.h
#import <UIKit/UIKit.h>

//oaconsumer
#import "OAConsumer.h"
#import "OADataFetcher.h"
#import "OAMutableURLRequest.h"

@interface OauthconsumerTestViewController : UIViewController  {
	IBOutlet UIButton*    btnLogin;              //ログインボタン
	IBOutlet UIWebView*   oauthWebView;               //認証用のブラウザ
	IBOutlet UILabel*     lblUser;               //USER表示
	IBOutlet UILabel*     lblAccessToken;        //ACCESS_TOKEN表示
	IBOutlet UILabel*     lblAccessTokenSecret;  //ACCESS_TOKEN_SECRET表示
	IBOutlet UITextField* fieldTweetContent;     //ツイート内容
	IBOutlet UIButton*    btnTweet;              //ツイートボタン
	
	//ライブラリのオブジェクト
	OAConsumer *consumer;
	OAToken *accessToken;
}

@property (retain, nonatomic)  UIButton*    btnLogin;             //ログインボタン
@property (retain, nonatomic)  UIWebView*   oauthWebView;               //認証用のブラウザ
@property (retain, nonatomic)  UILabel*     lblUser;              //USER表示
@property (retain, nonatomic)  UILabel*     lblAccessToken;       //ACCESS_TOKEN表示
@property (retain, nonatomic)  UILabel*     lblAccessTokenSecret; //ACCESS_TOKEN_SECRET表示
@property (retain, nonatomic)  UITextField* fieldTweetContent;    //ツイート内容
@property (retain, nonatomic)  UIButton*    btnTweet;             //ツイートボタン

//ライブラリのオブジェクト
@property (retain, nonatomic) OAConsumer *consumer;
@property (retain, nonatomic) OAToken *accessToken;

-(IBAction) btnLoginClicked:(id)sender;//ログインボタン押下
-(IBAction) btnTweetClicked:(id)sender;//ツイートボタン押下

@end
OauthconsumerTestViewController.m
#import "OauthconsumerTestViewController.h"

@implementation OauthconsumerTestViewController

@synthesize btnLogin, oauthWebView, lblUser, lblAccessToken, lblAccessTokenSecret, fieldTweetContent, btnTweet;
@synthesize consumer,accessToken;
#pragma mark -
#pragma mark View lifecycle
- (void)viewDidLoad {
	[super viewDidLoad];
	
	//画面表示をクリア
	lblUser.text = @"";
	lblAccessToken.text = @"";
	lblAccessTokenSecret.text = @"";
	fieldTweetContent.text=@"";
	
	//最初は認証用のブラウザを表示しない
	oauthWebView.hidden = YES;
}
#pragma mark -
#pragma mark button and screen click event
//ログインボタン押下
-(IBAction) btnLoginClicked:(id)sender{
	//承認開始
	[self performSelector:@selector(startRequestToken)];
}
//ツイートボタン押下
-(IBAction) btnTweetClicked:(id)sender{
	//認証後に
	if ([@"" isEqualToString:lblAccessToken.text] == NO && 
		[@"" isEqualToString:lblAccessTokenSecret.text] == NO) {
		//テキストフィールドの内容をツイート
		[self performSelector:@selector(tweet)];
	}
}
#pragma mark -
#pragma mark selectors for oauth requests
//認証開始 1.Request token(D)
-(void)startRequestToken {
	//CONSUMER_KEYとCONSUMER_SECRETの設定。Twitterのアプリケーション登録時に取得したものをセットしてください。
	NSString *CONSUMER_KEY = [NSString stringWithString:@"51G2KX3PSGMpmiv2mAxQ"];
	NSString *CONSUMER_SECRET = [NSString stringWithString:@"Mf8KMMtdGDB0NSBGEUeiKgSAn7YvpiHRUbMSW0yYrA"];
	
	self.consumer = [[OAConsumer alloc] initWithKey:CONSUMER_KEY
											 secret:CONSUMER_SECRET];

	OADataFetcher *fetcher = [[[OADataFetcher alloc] init]autorelease];

	NSURL *url = [NSURL URLWithString:@"https://api.twitter.com/oauth/request_token"];
	OAMutableURLRequest *request = [[[OAMutableURLRequest alloc] initWithURL:url
																	consumer:consumer
																	   token:nil
																	   realm:nil
														   signatureProvider:nil]autorelease];
	[request setHTTPMethod:@"POST"];
	
	[fetcher fetchDataWithRequest:request 
						 delegate:self
				didFinishSelector:@selector(requestTokenTicket:didFinishGetRequestToken:)
				  didFailSelector:@selector(requestTokenTicket:didFailTwitterSubmitterWithError:)];
}
// 1.Request token(D) 正常時 2.Authorize(F)
- (void) requestTokenTicket:(OAServiceTicket *)ticket didFinishGetRequestToken:(NSData *)data {
    if (ticket.didSucceed){

        NSString *responseBody = [[[NSString alloc] initWithData:data 
														encoding:NSUTF8StringEncoding]autorelease];
		
        self.accessToken = [[OAToken alloc] initWithHTTPResponseBody:responseBody];
		
        NSString *address = [NSString stringWithFormat:
							 @"https://api.twitter.com/oauth/authorize?oauth_token=%@&force_login=true",   //@"https://api.twitter.com/oauth/authenticate
                             accessToken.key];
		
        NSURL *url = [NSURL URLWithString:address];
		
		//ブラウザ表示
		oauthWebView.hidden = NO;
		[oauthWebView loadRequest:[NSURLRequest requestWithURL:url]];
		
    } else {
		//エラー処理	
	}
}
//エラー時
- (void) requestTokenTicket:(OAServiceTicket *)ticket didFailTwitterSubmitterWithError:(NSError *)error {
	//エラー処理	
}
// ブラウザから認証された場合 3.Access token(E)
- (void) backFromBrowser:(NSURL *)responseURL {
	
	//クエリからaccess tokenを取得
	self.accessToken = [self.accessToken initWithHTTPResponseBody:[responseURL query]];

	OADataFetcher *fetcher = [[[OADataFetcher alloc] init]autorelease];
	
	NSURL *url = [NSURL URLWithString:@"https://api.twitter.com/oauth/access_token"];
	OAMutableURLRequest *request = [[[OAMutableURLRequest alloc] initWithURL:url
																	consumer:consumer
																	   token:accessToken
																	   realm:nil
														   signatureProvider:nil]autorelease];
	[request setHTTPMethod:@"POST"];
	
	[fetcher fetchDataWithRequest:request 
						 delegate:self
				didFinishSelector:@selector(requestTokenTicket:didFinishfetchAccessToken:)
				  didFailSelector:@selector(requestTokenTicket:didFailTwitterSubmitterWithError:)];
}
- (void) requestTokenTicket:(OAServiceTicket *)ticket didFinishfetchAccessToken:(NSData *)data {
    if (ticket.didSucceed){
        NSString *responseBody = [[[NSString alloc] initWithData:data 
														encoding:NSUTF8StringEncoding]autorelease];
		
        self.accessToken = [self.accessToken initWithHTTPResponseBody:responseBody];

		//取得できた値を画面に表示する
		lblUser.text = self.accessToken.screen_name;
		lblAccessToken.text = self.accessToken.key;
		lblAccessTokenSecret.text = self.accessToken.secret;
    } else {
		//エラー処理	
	}
}
#pragma mark -
#pragma mark UIWebView events
// UIWebViewの内容がロード完了した時
- (void)webViewDidFinishLoad:(UIWebView *)webView {
	
	NSURL* url = [NSURL URLWithString:[webView stringByEvaluatingJavaScriptFromString:@"document.URL"]];
	
	//認証了の場合にコールバックされるURL Twitterのアプリ登録時に設定したURLです。
	NSURL *kanryoURL = [NSURL URLWithString:@"http://atsking.ats-japan.co.jp/~gau/index.html"];
	
	//ロードされたURLと、コールバックされるURLのhostが同じなら、承認されたと見なす
	if ([[url host] isEqualToString:[kanryoURL host]]) {
		//次の処理へ
		[self performSelector:@selector(backFromBrowser:) withObject:url];
		
		//webViewを隠す
		oauthWebView.hidden = YES;
	}	
}
#pragma mark -
#pragma mark tweet
//ツイートする
-(void)tweet {
	//ログイン認証された値を使用
	self.accessToken = [[[OAToken alloc] initWithKey:lblAccessToken.text
												  secret:lblAccessTokenSecret.text] autorelease];
	
	NSURL *url = [NSURL URLWithString:@"http://api.twitter.com/1/statuses/update.json"];
	OAMutableURLRequest* request = [[[OAMutableURLRequest alloc] initWithURL:url
																	consumer:consumer
																	   token:accessToken
																	   realm:nil
														   signatureProvider:nil] autorelease];
	
	[request setHTTPMethod:@"POST"];
	OARequestParameter *x1 = [[OARequestParameter alloc] initWithName:@"status" value:fieldTweetContent.text];
	
	NSArray *params = [NSArray arrayWithObjects:x1, nil];
	
	[request setParameters:params];
	
	OADataFetcher *fetcher = [[[OADataFetcher alloc] init] autorelease];
	[fetcher fetchDataWithRequest:request
						 delegate:self
				didFinishSelector:@selector(ticket:didFinishWithTweet:)
				  didFailSelector:@selector(ticket:didFailWithTweetError:)];
	[x1 release];	
}
- (void)ticket:(OAServiceTicket *)ticket didFinishWithTweet:(NSData *)data {
	fieldTweetContent.text=@"";
	//ツイート完了を知らせる
}

- (void)ticket:(OAServiceTicket *)ticket didFailWithTweetError:(NSError *)error {
	//エラー処理	
}
// 以下省略
@end

oauthconsumerへのちょっとした追加

Access tokenのレスポンスには、ユーザー名(screen_id)も帰ってくるので、これを取得しておきたいです。
上記コード内の requestTokenTicket:ticket didFinishfetchAccessToken:dataメソッド内で、引数のdataを直接解析してもいいのですが、OATokenクラス内でデータの解析を行っているため、真似して追加してみます。

OAToken.h

クラスフィールドに NSString *screen_nameを追加します。

OAToken.m
- (id)initWithHTTPResponseBody:(const NSString *)body {

//...中略

	for (NSString *pair in pairs) {
        NSArray *elements = [pair componentsSeparatedByString:@"="];
        if ([[elements objectAtIndex:0] isEqualToString:@"oauth_token"]) {
            aKey = [elements objectAtIndex:1];
//...中略
		} else if ([[elements objectAtIndex:0] isEqualToString:@"oauth_token_renewable"]) {
			NSString *lowerCase = [[elements objectAtIndex:1] lowercaseString];
			if ([lowerCase isEqualToString:@"true"] || [lowerCase isEqualToString:@"t"]) {
				renew = YES;
			}
		}
		
		//追加
		else if ([[elements objectAtIndex:0] isEqualToString:@"screen_name"]) {
            self.screen_name = [elements objectAtIndex:1];
		}

    }
    
    return [self initWithKey:aKey secret:aSecret session:aSession duration:aDuration
				  attributes:attrs created:creationDate renewable:renew];
}

5が月前に取得したライブラリでは、oauth_verifierが取得できなかったため、同じようにして追加しました。今後twitter側で仕様が変更され、新たなキーを取得しないといけなくなったら、またこのクラスをいじればある程度対応できるのではないでしょうか。

実行

f:id:Aodrey:20110613055010p:image:w240
起動画面です。UIWebViewはhidden=YESによって表示されません。
f:id:Aodrey:20110613055009p:image:w240
「ログイン認証する」ボタンを押すことで、UIWebViewが表示されます。
f:id:Aodrey:20110613055008p:image:w240
ログインID、パスワードを入力し、「アプリを認証」ボタンを押します。
f:id:Aodrey:20110613055007p:image:w240
f:id:Aodrey:20110613055006p:image:w240

ログインできたので、Access トークンが取得できました。
f:id:Aodrey:20110613055005p:image:w240
さっそくつぶやいてみます。テキストフィールドに入力し、Tweetボタンを押下。
f:id:Aodrey:20110613055004p:image:w240
ツイートできたみたいです。
f:id:Aodrey:20110613055003p:image:w240
ごらんの通り。

プロジェクト一式

こちらからどうぞ。
OauthconsumerTest.zip 直