TwitterにポストするiPhoneアプリをBasic認証からxAuthに変更してみました

TwitterAPIアクセスの 6月でBasic認証を廃止し OAuthのサポートのみになります。ただし、OAuthは認証手順の中でブラウザーを使ってTwitter側で認証する必要があり iPhoneアプリ等ではたいへんな手間になります。その為にOAuthの簡易版のような xAuthがサポートされました
そこで 私の作ったiPhoneアプリTweetなう」をBasic認証からxAuthに変更してみました。

1. アプリを Twitterに登録する

a) xAuthを使うには、まず http://twitter.com/oauth_clients/new でアプリをTwitterに登録する必要があります(もちろんTwitterのアカウントも必要です)。
b) 登録後に表示される登録アプリ情報ページにOAuthで使う Consumer key や Consumer secret を始め重要な情報が書かれいるので保存しておきましょう。
c) ここで注意する点として、ここで登録しても OAuthしか使えせん。 登録アプリ情報ページの下の方に書いているようにメールでxAuthを有効にしてくれるようにリクエストする必要がります。

Note: xAuth is not enabled for applications by default. Email api@twitter.com to request xAuth access.

私の場合は、登録アプリ情報ページのURL と twitterのアカウントを書いた簡単なメールを送りましたが、次の日に有効になったよメールが来ました。

2. XAuthTwitterEngine を使ってみる

xAuth の手順は複雑なので、既存のライブラリーを使うのがお手軽だと思います。ここでは XAuthTwitterEngine を使ってみました。このライブラリーには Twitter API を簡単に使える MGTwitterEngine が使われています(含まれています)。

a) まずは XAuthTwitterEngine を clone し中にあるデモアプリを動かしてみて下さい。ただし、自分のアプリの Consumer key と Consumer secret を設定してあげる必要があります。
b) TwitterのID,Passwordを設定し、 1. Get xAuth Access Token をクリックし、2. Send test tweet をクリックするとTwitterにアプリ名で Testing xAuth from the XAuthTwitterEngineDemo! とつぶやかれます。

3. XAuthTwitterEngine を組み込む

デモアプリを参考に下のようなヘルパークラスを作りました。デモアプリから変更した点は:

  1. 1コールでポストできるようにした
  2. アクセストークンのキャッシュはしない。「Tweetな」は 単純にTwitterにつぶやくだけのアプリなのでセキュリティー的に注意が必要な情報のキャッシュは止めてしまいました。
  3. タイムアウト検出の追加
  4. エラーメッセージの変更。エラーの内容を表示しても通常のユーザーにはあまり役に立たないかなと思いシンプルしました。

注意点としては MGTwitterEngine はXMLJSONの両方に対応出来るように書かれているのですが、そのままコンパイルすると YAJLのライブラリが無いエラーになります。XMLでやり取りするなら、ターゲット(ソース)からMGTwitter*YAJLParser.mを削除して libxml2.dylib をリンクして下さい。 MGTwitterEngineを使ってラクラクTwitter連携 - Tomute’s Notes に詳しく書かれています。


まだ、十分なテストは行ってませんのでバグがあるかも ^^);

5月12日追記タイムアウトの処理にバグがあったのでコードを変更しました。
5月15日追記delegate オブジェクトは retain すべきではない - 24/7 twenty-four seven を見てコード変更、ありがとうざいます!


XAuthTwitterStatusPost.h

#import <Foundation/Foundation.h>
#import "XAuthTwitterEngineDelegate.h"

@class XAuthTwitterEngine;

@interface XAuthTwitterStatusPost : NSObject  <XAuthTwitterEngineDelegate> {
    id       delegate;
    SEL      didSuccessSelector;
    SEL      didFailedSelector;
  @private
    XAuthTwitterEngine *twitterEngine;
    NSString *username;
    NSString *password;
    NSString *message;
}
@property (nonatomic, assign) id       delegate;
@property (nonatomic)         SEL      didSuccessSelector;
@property (nonatomic)         SEL      didFailedSelector;

- (XAuthTwitterStatusPost *) init;
- (void) setUserName:(NSString *)aUsername password:(NSString *)aPassword;
- (void) postMessage:(NSString *)aMessage timeoutInterval:(NSTimeInterval)timeoutInterval;

@end

XAuthTwitterStatusPost.m

#import "XAuthTwitterStatusPost.h"
#import "XAuthTwitterEngine.h"
#import "XAuthInformations.h"
#import "SimpleAlertView.h"                  // シンプルなアラート表示


#pragma mark Private properties and methods definition
@interface XAuthTwitterStatusPost ()

@property (nonatomic, retain) XAuthTwitterEngine *twitterEngine;
@property (nonatomic, retain) NSString *username;
@property (nonatomic, retain) NSString *password;
@property (nonatomic, retain) NSString *message;

- (void) timeOutAction;
- (void) errorAlert:(NSError *)error;
@end


#pragma mark -
@implementation XAuthTwitterStatusPost
@synthesize twitterEngine;
@synthesize username;
@synthesize password;
@synthesize message;
@synthesize delegate;
@synthesize didSuccessSelector;
@synthesize didFailedSelector;


#pragma mark Memory management methods.

- (void)dealloc {
    [twitterEngine release];
    [username release];
    [password release];
    [message release];
    [super dealloc];
}


#pragma mark public methods

- (XAuthTwitterStatusPost *) init {
    self = [super init];
    if (self) {
        self.twitterEngine = [[XAuthTwitterEngine alloc] initXAuthWithDelegate:self];
        self.twitterEngine.consumerKey = kOAuthConsumerKey;
        self.twitterEngine.consumerSecret = kOAuthConsumerSecret;
        self.delegate = nil;
        self.didSuccessSelector = nil;
        self.didFailedSelector = nil;
    }
    return self;
}

- (void) setUserName:(NSString *)aUsername password:(NSString *)aPassword {
    self.username = aUsername;
    self.password = aPassword;
}


- (void) postMessage:(NSString *)aMessage timeoutInterval:(NSTimeInterval)timeoutInterval {
    self.message = aMessage;

    // NSLog(@"exchangeAccessTokenForUsername %@", self.username);
    [self.twitterEngine exchangeAccessTokenForUsername:self.username password:self.password];
    if (timeoutInterval != 0.0) {
        [self performSelector:@selector(timeOutAction) withObject:nil afterDelay:timeoutInterval];
    }
}


#pragma mark XAuthTwitterEngineDelegate methods


- (void) sendUpdateToTwitter:(id)anArgument {
    // NSLog(@"sendUpdate: %@", self.message);
    [self.twitterEngine sendUpdate:self.message];
}

- (void) storeCachedTwitterXAuthAccessTokenString: (NSString *)tokenString forUsername:(NSString *)username {
    // NSLog(@"Access token string returned: %@", tokenString);
    [self performSelector:@selector(sendUpdateToTwitter:) withObject:nil afterDelay:0.1];
}

- (NSString *) cachedTwitterXAuthAccessTokenStringForUsername: (NSString *)username {
    // NOT cache the xAuthAccess token
    return nil;
}

- (void) twitterXAuthConnectionDidFailWithError: (NSError *)error {
    [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(timeOutAction) object:nil];
    NSlog(@"twitterXAuthConnectionDidFailWithError: %@", error);
    [self errorAlert:error];
    
    // callback
    if (self.delegate && self.didFailedSelector) {
        [self.delegate performSelector:self.didFailedSelector withObject:nil afterDelay:0.1];
    }
}


#pragma mark MGTwitterEngineDelegate methods

- (void)requestSucceeded:(NSString *)connectionIdentifier {
    [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(timeOutAction) object:nil];
    NSlog(@"Twitter request succeeded: %@", connectionIdentifier);
    // callback
    if (self.delegate && self.didSuccessSelector) {
        [self.delegate performSelector:self.didSuccessSelector withObject:nil afterDelay:0.1];
    }
}

- (void)requestFailed:(NSString *)connectionIdentifier withError:(NSError *)error {
    [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(timeOutAction) object:nil];
    NSlog(@"Twitter request failed: %@ with error:%@", connectionIdentifier, error);
    
    [self errorAlert:error];
    // callback
    if (self.delegate && self.didFailedSelector) {
        [self.delegate performSelector:self.didFailedSelector withObject:nil afterDelay:0.1];
    }
}


#pragma mark Private methods

- (void) timeOutAction {
    
    NSlog(@"Twitter request Timout");

    [SimpleAlertView alertWithTitle:@"タイムアウト" message:@"時間をおいてから再度つぶやいて下さい"];
    // callback
    if (self.delegate && self.didFailedSelector) {
        [self.delegate performSelector:self.didFailedSelector withObject:nil afterDelay:0.1];
    }    
}

- (void) errorAlert:(NSError *)error {
    [SimpleAlertView alertWithTitle:[NSString stringWithFormat:@"Twitter送信エラー (%d)", [error code]]
                     message:@"ログイン/パスワードが正しくない、Twitterサービス、通信などが考えられます"];
}


@end