ActionScript3でTwitter連携
【AIR for iOS/AIR for Android/desktop AIR対応】

マッシュアップ(て言うんでしょうか。マッシュアップの意味あんまりわかってなくて使ってます)系コンテンツを制作するときによく問題となるのがクロスドメインポリシーですが、AIRではこのクロスドメインの制限からかなり解放されています。
そこで、(クロスドメインでの通信を透過させる目的でよく使っている、PHPなどの外部プログラムを必要とせず)ActionScript3(以下AS3)だけでTwitterと連携できないか試してみました。

ご存じのとおりTwitterAPIがバージョン1.1になって、使用回数の制限なんかがかなり厳しくなった今、スマホやデスクトップで動くTwitterアプリ作る人なんてあまりいないとは思います。このブログはいつも話題について書く時期を間違えるんです。
しかしながら、ただでさえAS3とTwitterとの連携について国内外を問わず記事やサンプルが少ないですし、あったとしても言語がAS3に限らずどの言語においてもAPIのバージョンが1.0のときのものが多く1.1については情報がほとんどなかったので、自分のやった覚え書き程度は残しておこうかと思います。
自分が同じところで二度と詰まらないように、「どうしてこう書くような考えに至ったか」までを書いているので長いですが、そのものズバリなコードも一緒に書いてますので、説明を読むのが面倒臭い場合はそのままコピペでも動くようになっているとは思います。 

あと理想を言えば、TwitterAPI界でのAS3がわりと空気なので、他の言語よりも詳しくAPI1.1についての情報を出しておきたいというのもあります。Twitter使うならAIRでAS3がいちばん情報でてるでしょ。てな具合に。

ちなみにAS3でAPIを直接叩く方法以外に、AIR for iOS/AIR for AndroidではANEによってOSネイティブのTwitter機能を使うことが出来ます。
私のみつけた範囲ではこちらに既に作ったANEを販売しているサイトがあります。 
実際にiOS/Android両方版を買ってみましたが、 ANEを組み込む方法からASDocまで揃ってて、ドキュメントもリファレンスもしっかりしていてとてもおすすめです!

で、この金の力にものをいわせて購入したANEを使っても良いのですが、それを使わずに今回はAS3で直接TwitterAPIを叩く方法をとりたいと思います。
APIを直接たたく方法だとスマホだけでなくデスクトップAIRでも使えるし、モバイルOSがサポートしている機能だけでなくTwitterAPIの全機能が使えるようになるし、OAuthの勉強にもなるしで、総合的にみて自由度が高いと思ったからです。
なにより「それ、AIRだけでできるよ」と言えるのが気持ちがいいです。 


まずOAuth認証についてざっくりと理解してみる

Twitterと連携させるためにはOAuth認証をしないといけないのですが、まずこのOAuth認証について、アプリ開発に困らん程度には理解しておく必要があったのでまとめてみました。
登場人物は3人。

  •  Developer登録をしたアプリ。正確な名称ではOAuthコンシューマーと呼ばれる。
  • OAuthでサービスを提供しているAPI。本記事ではTwitterAPIのこと。OAuthサービスプロバイダーと呼ばれる。
  • ユーザー。アプリを使ってくれるひとたち。

上記3人がくりひろげる認証のための物語の流れはこんなかんじ。

  1. Developer登録をしたアプリにはコンシューマーKeyとコンシューマーsecretが割り当てられる。
    1. このコンシューマーkeyとsecretの組み合わせが、自分のアプリの証明書となる。
    2. この組み合わせをコンシューマートークンと呼ぶ。
    3. secretは文字通り秘密鍵なので他人にはひみつ。keyとsecretの関係はそのままIDとpasswordの関係と同じ。
  2. ここまではAPI側が提供している、Developer登録画面で行われる。
  3. まずは、アプリを(APIと連携させるために)ユーザーに認証してもらう必要があるため、ユーザーに表示する認証画面をAPIに作ってもらう必要がある。
    1. そのためにリクエストトークンというトークンを発行するようにAPIに依頼する。
    2. その際に自分のアプリである証明のために、コンシューマートークンをAPIに対して送る。
      secretをそのまま送るわけにはいかないので、暗号化して送る。
    3. APIはコンシューマーの証明をすませたのち、リクエストトークンを発行する。
    4. このとき発行されるリクエストトークンを、未認可リクエストトークンと呼ぶ。
  4. ここまではユーザーの見えないバックグラウンドで行われる。
  5. アプリはリクエストトークンを受け取ったら、リクエストトークンから得られる情報を元に認証画面を表示する。
    1. 基本的にはAPIによって定められたURLにパラメーターとしてリクエストトークンを渡すかたち。 
  6. ユーザーは認証画面を確認し、納得のいくものであればアプリを自分のアカウントで使ってもいいということを認証する。
  7. ユーザーからの認可が得られた場合、アプリに認可済みリクエストトークンが発行される。
    1. これは文字通り、ユーザーが認可したという証明付きのリクエストトークン。
    2. 認証画面からのコールバックURLをアプリが指定していた場合、リクエストトークンのパラメーター付きでリダイレクトされる。
    3. そうでない場合、認可済み証明書はPINコードの形で表示され、ユーザーに対してこのPINコードをアプリに直接入力するよう促される。
  8. リダイレクトの引数の場合でも、PINコード入力の場合でも、ユーザーからの認可証明をアプリはいずれかの形で手に入れ、その認可済みリクエストトークンをバックグランドでAPIに渡し、アクセストークンを発行してもらう。

こんな感じです。
おおまかな流れとしては、ユーザーの認可済みリクエストトークンをもらって、それを引換券としてAPIにアクセストークンをもらうという形になります。
アプリ側はこのアクセストークンを保存しておけば、このトークンは永久的に有効なので、ユーザーがアプリの連携を解除しない限りはアプリ側でもうOAuth認証は必要無くなります。 

以後は、コンシューマートークンとアクセストークンをペアにして、APIを叩くことになります。

OAuth認証について、開発に困らない程度の理解度で言えばこんな感じでした。
ちょっとややこしいですが、実際にコードを書いていくと動きがすんなりとわかると思います。 


アプリ登録

ということでまずアプリ登録ですが、これは簡単でした。

TwitterのDeveloper画面にいって、ログイン後アプリを登録するだけです。

TwitterDevelopers画面

画面右上のドロップダウンメニューから「My applications」→「Create a new application」

CAPTCHA

フォームののところをいいかんじに埋めて、規約をよく読み同意して、CAPTCHA入力したらOKです。


ライブラリ選び

AS3で使用するライブラリには、twitter-actionscript-apiを選びました。
なぜかというとそれしか見つからなかったからです…。
他にみつかっても既に公開終了してたり…。
あとでも書きますが、twitter-actionscript-apiにしてもTwitterAPIのバージョンが1のまま止まっているので、そのまま使ってもAPI1.1では動かないところが多々あります。

Twitter界におけるAS3の疎外感すごい…。

とりあえずプロジェクトに対してtwitter-actionscript-apiのパスを通して、開発スタートです。

あとサンプルにはswcも含まれていますが、実際に使うときはソースのほうを使った方が良いです。
開発中、うまくいかないときは直接ライブラリのソースを読むことが多くなるので、プロジェクトそのものにソースのほうを使っておけばFlashBuilderだとF3キーで呼び出し元の参照を辿れたり、何かと便利だからです。


実際にOAuth認証してみる

ライブラリを使えば、OAuth認証に関してはめっちゃ簡単でした。

ライブラリに入ってあるサンプルを参考に、そのまんまな感じで進めます。

var twitterApi:TwitterAPI = new TwitterAPI();

まずapiのインスタンスを作ります。

twitterApi.connection.addEventListener(OAuthTwitterEvent.REQUEST_TOKEN_RECEIVED, handleRequestTokenReceived);
twitterApi.connection.authorize(CONSUMER_KEY, CONSUMER_SECRET);

認証開始。
私が少し詰まったのは、間違ってapiに直接addEventListenerしてしまって、リクエストトークンを受け取った通知が拾えなかったことでした。
サンプルコードをよく読むべきでした。実際にはapiのconnectionプロパティに対してイベントリスナーを設定します。

function handleRequestTokenReceived(event:OAuthTwitterEvent):void
{
	navigateToURL(new URLRequest(twitterApi.connection.authorizeURL));
}

受け取ったリクエストトークンをもとに、認証画面をブラウザで表示します。
サンプルではアプリ内のWebViewに認証画面を直接表示していましたが、アプリ内でTwitterのIDとPasswordを入力させるとOAuthの意味がないので、ここは必ずブラウザに飛ばすようにします。

参考:

アプリ内でWebViewを使うとURLが表示されません.つまり 本当にツイッターにアクセスしているかわからない のです. もし,表示されるのが偽の認証画面だったら,アプリから簡単にパスワードがわかってしまいます.

じゃあ,URL を表示させればいいかというとそういうわけでもありません. 画面上のURL表示なんて簡単に偽装できてしまいます. どんな工夫をしても アプリがパスワードの要求をしていることには変わりありません . アプリはパスワードを簡単に取得できます.

OAuthの認証にWebViewを使うのはやめよう

あとはユーザーに認証してもらって、表示されるPINコードを入力してもらいます。

twitterApi.connection.addEventListener(OAuthTwitterEvent.AUTHORIZED, handleAuthorized);
var pinCode:String;//ユーザーに入力してもらったPINコード
twitterApi.connection.grantAccess(pinCode);

ユーザーが認証したことを証明するPINコードと引き替えに、アクセストークンを要求します。

function handleAuthorized(event:OAuthTwitterEvent):void
{
	var accessToken:OAuthToken = twitterApi.connection.accessToken;
}

あとはこんな感じでアクセストークンが取り出せるようになります。

とくに詰まるところもなく、ほぼサンプルと同じような流れでいけると思います。


もっと簡単に認証できるようにする

さきほど試した方法は、ユーザーが認証したことをアプリが知るために、ユーザー認証証明書(oauth_verifier)をPINコードとしてユーザーに入力してもらう必要がありました。
そのこと自体は、Twitterの認証の仕組みとして当たり前の流れだし、デスクトップAIRならそれまでなのでいいんですが、AIR for iOSやAIR for Androidでもっと便利な方法をとってみたいと思います。

細かく項目を分けると次のような流れになります。

  • もっと便利な方法が無いか検証してみる
  • カスタムURLスキームを使う
  • カスタムURLスキームにコールバックを指定する
  • 実際にコードを書いていく

もっと便利な方法が無いか検証してみる

アプリの認証の方法についてのルールを順番に考えていくと、

  1. ユーザー認証が終わったあと、コールバックURLが指定されていればそのURLにリダイレクトされる。
  2. WEBアプリならいざ知らず、デスクトップアプリやモバイルアプリにはURLとか無いェ…。
  3. コールバックURLが無いなら、認証されたことをアプリが知るためには認証証明をPINコードとしてユーザーに打ち込んでもらうしか無いよね…。

だったんですが、この2番の「アプリにはURLが無い」のであきらめてたところ、実は方法がありました。

カスタムURLスキームを使う方法です。

カスタムURLスキームを使う

AIR for iOSやAIR for Android(以後モバイルAIRアプリ)では、カスタムURLスキームを使うことが出来るんです。
カスタムURLスキームについて参考になる記事がこちらにあります。

Defining custom URL schemes for your AIR mobile applications

リンク先は英語ですが、設定XMLや簡単なサンプルコードばかりなので、わかりやすいと思います。

つまり、コールバックURLにこのスキーマーを指定してやれば、認証後に必要な証明をアプリ側はパラメーターとして受け取ることが出来るというわけです。

カスタムURLスキームにコールバックを指定する

実際にやってみたいと思います。方法としては、Developer登録画面でコールバックURLを指定する方法と、最初の認証用リクエストトークンの発行を依頼するときに一緒にパラメーターとしてコールバックURLを指定する方法の二通りがあります。この二通りの方法を組み合わせると、

  • Develorer画面で指定しないパラメーターでも指定しない
    • まちがい。
    • 何も指定しないと、先ほどやったPIN入力になります。
  • Developer画面で指定しないパラメーターで指定する
    • まちがい。
    • Developer画面で指定しないと、Twitterは「コールバックURLが無いならWEBアプリじゃなくてデスクトップアプリかモバイルアプリなんだな」と判断してしまいます。
      そしてリクエストトークン発行依頼を受け付けたときに「コールバックURLはWEBアプリでしか受け付けませんが?」と怒られ、エラーになります。
  • Developer画面で指定するパラメーターでは指定しない
    • まちがい。
    • Developer画面では「http:」「https:」のスキーマーしか受け付けてくれず、カスタムスキームを受け付けてくれません。
  • Developer画面で適当なURLを入力しといてパラメーターでカスタムURLスキームを指定する
    • せいかい。
    • Developer画面に入力したコールバックURLよりも、リクエストトークン発行依頼時に指定したコールバックURLのほうが優先されるので、この組み合わせが有効です。
      Developer画面のほうに入力したコールバックURLは、この仕組み上いれてるだけのダミーなので、とびきりえっちなサイトのURLでも入れておきましょう。 

この組み合わせ以外だと受け付けないので、これを探り当てるのにまず苦労しました。

実際にコードを書いていく

さて、方法がわかったら実際にコードを書いていくことにします。

AS3で使用しているTwitterライブラリ「twitter-actionscript-api」は、そのままではリクエストトークン発行依頼時にコールバックURLを指定することができません。
なのでライブラリを拡張して書いていくことにします。 

OAuthTwitterConnection2
package twitter
{
	import com.dborisenko.api.twitter.oauth.OAuthTwitterConnection;
	import com.dborisenko.api.twitter.oauth.events.OAuthTwitterEvent;
	
	import mx.rpc.events.FaultEvent;
	import mx.rpc.events.ResultEvent;
	import mx.rpc.http.HTTPService;
	
	import org.iotashan.oauth.OAuthRequest;
	import org.iotashan.oauth.OAuthSignatureMethod_HMAC_SHA1;
	
	[Event(name="requestTokenError",type="com.dborisenko.api.twitter.oauth.events.OAuthTwitterEvent")]

	[Event(name="requestTokenReceived",type="com.dborisenko.api.twitter.oauth.events.OAuthTwitterEvent")]

	[Event(name="accessTokenError",type="com.dborisenko.api.twitter.oauth.events.OAuthTwitterEvent")]

	[Event(name="authorized",type="com.dborisenko.api.twitter.oauth.events.OAuthTwitterEvent")]

	public class OAuthTwitterConnection2 extends OAuthTwitterConnection
	{
		//ついでにプロトコルもhttpsにしちゃう
		private static const REQUEST_TOKEN_URL:String = "https://twitter.com/oauth/request_token";
		private static const ACCESS_TOKEN_URL:String = "https://twitter.com/oauth/access_token";
		private static const AUTHORIZE_URL:String = "https://twitter.com/oauth/authorize";
		
		private var _authorized:Boolean = false;
		private var signatureMethod:OAuthSignatureMethod_HMAC_SHA1 = new OAuthSignatureMethod_HMAC_SHA1();
		
		private var userId:String;
		private var screenName:String;
		
		/**
		 * @constructor
		 */
		public function OAuthTwitterConnection2()
		{
			super();
		}
		
		/**
		 * リクエストトークンも受け取れるように改造
		 */
		public function setRequestToken(requestKey:String, requestSecret:String):void
		{
			requestToken.key = requestKey;
			requestToken.secret = requestSecret;
		}
		
		/**
		 * コールバックURLを受け取るように改造
		 */
		public function authorize2(consumerKey:String, consumerSecret:String, callbackUrl:String = null):void
		{
			if (_authorized)
				return;
			
			consumer.key = consumerKey;
			consumer.secret = consumerSecret;
			var params:Object = null;
			if(!(!callbackUrl)){
				params = {oauth_callback:callbackUrl};
			}
			getRequestToken(params);
		}
		
		/**
		 * コールバック用にパラメーターを受け取るように改造
		 */
		private function getRequestToken(params:Object = null):void
		{
			if (_authorized)
				return;
			
			var oauthRequest:OAuthRequest = new OAuthRequest(OAuthRequest.HTTP_METHOD_GET, REQUEST_TOKEN_URL, params, consumer);
			var url:String = oauthRequest.buildRequest(signatureMethod);
			
			var service:HTTPService = new HTTPService();
			service.url = url;
			service.addEventListener(ResultEvent.RESULT, handleRequestToken);
			service.addEventListener(FaultEvent.FAULT, handleRequestTokenError);
			service.resultFormat = HTTPService.RESULT_FORMAT_TEXT;
			service.send();
		}
		
		private function handleRequestToken(event:ResultEvent):void
		{
			var params:Array = event.result.toString().split("&");
			for (var i:int = 0; i < params.length; i++) 
			{
				var param:String = params[i];
				var nameValue:Array = param.split("=");
				if (nameValue.length == 2) 
				{
					switch (nameValue[0]) 
					{
						case "oauth_token":
							requestToken.key = nameValue[1];
							break;
						case "oauth_token_secret":
							requestToken.secret = nameValue[1];
							break;
						default:
					}
				}
			}
			dispatchEvent(new OAuthTwitterEvent(OAuthTwitterEvent.REQUEST_TOKEN_RECEIVED, requestToken.key));
		}
		
		private function handleRequestTokenError(event:FaultEvent):void
		{
			dispatchEvent(new OAuthTwitterEvent(OAuthTwitterEvent.REQUEST_TOKEN_ERROR, event.fault.message));
		}
		
		private function getAccessToken(pin:String):void 
		{
			if (_authorized)
				return;
			
			var oauthRequest:OAuthRequest = new OAuthRequest(OAuthRequest.HTTP_METHOD_GET, ACCESS_TOKEN_URL, {oauth_verifier: pin}, consumer, requestToken);
			var url:String = oauthRequest.buildRequest(signatureMethod);
			
			var service:HTTPService = new HTTPService();
			service.url = url;
			service.addEventListener(ResultEvent.RESULT, handleAccessToken);
			service.addEventListener(FaultEvent.FAULT, handleAccessTokenError);
			service.resultFormat = HTTPService.RESULT_FORMAT_TEXT;
			service.send();
		}
		
		private function handleAccessToken(event:ResultEvent):void
		{
			var params:Array = event.result.toString().split("&");
			for (var i:int = 0; i < params.length; i++) 
			{
				var param:String = params[i];
				var nameValue:Array = param.split("=");
				if (nameValue.length == 2) 
				{
					switch (nameValue[0]) 
					{
						case "oauth_token":
							accessToken.key = nameValue[1];
							break;
						case "oauth_token_secret":
							accessToken.secret = nameValue[1];
							break;
						case "user_id":
							userId = nameValue[1];
							break;
						case "screen_name":
							screenName = nameValue[1];
							break;
						default:
					}
				}
			}
			_authorized = true;
			dispatchEvent(new OAuthTwitterEvent(OAuthTwitterEvent.AUTHORIZED));
		}
		
		private function handleAccessTokenError(event:FaultEvent):void
		{
			dispatchEvent(new OAuthTwitterEvent(OAuthTwitterEvent.ACCESS_TOKEN_ERROR, event.fault.message));
		}
	}
}

ライブラリの外から見ると、authorize2というメソッドを追加しています。
難しかったのが、最初コールバックURLを指定するタイミングがわからず、リクエストトークンを受け取ってから認証画面に遷移する段階でパラメーターを渡してしまい、正常に動作しなかったことです。
公式ドキュメントを読めば、ちゃんとリクエストトークン発行依頼時に指定しなはれと書いてあるのですが、それよりも認証画面表示のときに指定するものという先入観が勝ってしまい正解を得るまで時間がかかってしまいました。

もとのライブラリが、拡張して使われる前提では書かれていないので、というより自分がライブラリ拡張のスキルが足りないのでわりと無理矢理に拡張させています。
クラスの名前も、なんたら2とか数字足していくのどうなんだって感じですが、名前に機能を示すあまりOAuthTwitterConnectionSupportCallbackURLParameterWhenRequestTokenみたいな長い名前になっても嫌なので、悩みどころです。
まあ考えようによっては、芸もなくなんたら2とか数字つけてる時点で「このクラスはもとのクラスからちょっと機能追加されたクラスなんだな」とわかるという意味では、この命名規則もアリなんでしょうか。自分でもやってて自信がないんですが、ここらへんはちょっとわからないです。

んで、このOAuthTwitterConnection2を扱うクラスも拡張。

TwitterAPI2
package twitter
{
	import com.dborisenko.api.twitter.TwitterAPI;
	import com.dborisenko.api.twitter.oauth.OAuthTwitterConnection;
	import com.dborisenko.api.twitter.twitter_internal;
	
	use namespace twitter_internal;
	
	public class TwitterAPI2 extends TwitterAPI
	{	
		/**
		 * 独自に拡張したOAuthTwitterConnectionです。
		 */
		protected var _connection2:OAuthTwitterConnection2 = new OAuthTwitterConnection2();

		/**
		 * 独自に拡張したOAuthTwitterConnectionを返します。
		 */
		override public function get connection():OAuthTwitterConnection
		{
			return _connection2;
		}

		/**
		 * @constructor
		 */
		public function TwitterAPI2()
		{
			super();
		}
	}
}

これでライブラリはリクエストトークン発行以来時にコールバックURLを指定できるようになりました。
あとはアプリは次のような流れで進めていきます。

const _CALLBACK_URL:String = 'myAppScheme://auth';
var _api = new TwitterAPI2();
//認証を開始する
function startAuth():void
{
	_api.connection.setConsumer(KEY, SECRET);
	_api.connection.addEventListener(OAuthTwitterEvent.REQUEST_TOKEN_RECEIVED, function(e:OAuthTwitterEvent):void{
		_api.connection.removeEventListener(OAuthTwitterEvent.REQUEST_TOKEN_RECEIVED, arguments.callee);
		var requestToken:OAuthToken = _api.connection.requestToken;
		//ブラウザに飛ぶので、一旦リクエストトークンをSharedObjectに保存
		SOManager.getInstance().setRequestToken(requestToken.key, requestToken.secret);
		//受け取った認証用トークンをもとに認証画面へ飛ぶ
		navigateToURL(new URLRequest(_api.connection.authorizeURL));
	});
	//コールバックを指定して認証用トークンの発行を依頼
	OAuthTwitterConnection2(_api.connection).authorize2(KEY, SECRET, _CALLBACK_URL);
}

さきほど作ったauthorize2というメソッドを使って、コールバックURLを指定して認証開始しています。

ちなみにTwitterAPIと直接は関係ないですが、リクエストトークンの一時保管やアクセストークンの記録のためにSharedObjectを使用しています。
SharedObjectについてすごく今さらな話題ですが、私は一括で管理するSharedObjectManagerというクラスを作って、そこに丸投げする形をとっています。

SOManager
package data
{
	import flash.net.SharedObject;
	import flash.utils.flash_proxy;
	
	import org.iotashan.oauth.OAuthToken;

	public class SOManager
	{
		private static const _SO_NAME:String = 'twitterTest0000';
		private static const _REQUEST_TOKEN_NAME:String = 'requestToken';
		private static const _ACCESS_TOKEN_NAME:String = 'accessToken';
		
		private var _so:SharedObject;

		public function set accessToken(value:OAuthToken):void
		{
			_so.data[_ACCESS_TOKEN_NAME] = value;
			_so.flush();
		}
		
		public function get accessToken():OAuthToken
		{
			return _so.data[_ACCESS_TOKEN_NAME] as OAuthToken;
		}
		
		public function setRequestToken(key:String, secret:String):void
		{
			var token:OAuthToken = new OAuthToken(key, secret);
			_so.data[_REQUEST_TOKEN_NAME] = token;
			_so.flush();
		}
		
		public function get requestToken():OAuthToken
		{
			return _so.data[_REQUEST_TOKEN_NAME];
		}
		
		
		private static var _instance:SOManager;
		
		public static function getInstance():SOManager
		{
			if(!SOManager._instance){
				SOManager._instance = new SOManager(new Enforcer());
			}
			if(!SOManager._instance._so){
				SOManager._instance._so = SharedObject.getLocal(_SO_NAME);
			}
			return SOManager._instance;
		}
		
		public function SOManager(enforcer:Enforcer)
		{
		}
	}
}
class Enforcer{}

こうしておくと、どのクラスがどこでSharedObjectを使用しているか追跡可能になるのがいちばん便利です。
ほかにも利点としてはクラス内ではso.data[KEY]こういう形でアクセスするように書くことで、SharedObjectのデーター内のKEYのtypoによるバグを防げます。
悩んでいるのが、SharedObjectそのものがシングルトンなのに、それを管理するマネージャークラスもシングルトンて、何か変じゃない?私のクラス、シングルトン使いすぎ…?ということです。
いろいろ迷ったのですが、SharedObjectというシングルトンを管理するクラスがインスタンスを複数生成してしまうのも何かメモリの無駄なような気がして、それで今のところはマネージャークラスもシングルトンで作る事に落ち着いています。

SharedObjectについての話題はこれまでにして。あとは、ユーザーがアプリを認証するとコールバックとしてカスタムURLスキームからアプリが呼ばれます。そこから引数として認証証明が受け取れますので、こんな感じに書いてやればいい感じになりました。

//スキーマーから立ち上がって引数があった場合、ユーザーの認証が完了してるので引数を受け取る
NativeApplication.nativeApplication.addEventListener(InvokeEvent.INVOKE, function(e:InvokeEvent):void{
	if(e.arguments.length > 0){
		setAuth(e.arguments[0] as String);
	}
});

あとはこの引数から認証証明を取り出して、その認証証明と引き替えにアクセストークンを受け取れば完了です。

//認証を完了する
function setAuth(param:String):void
{
	//パラメーターから認証証明を取得
	var p:Object = getParams(param);
	var verifier:String = p['oauth_verifier'];
	//一旦保存してあったリクエストトークンを取得
	var requestToken:OAuthToken = SOManager.getInstance().requestToken;
	//コンシューマートークンとアクセストークンを設定
	_api.connection.setConsumer(KEY, SECRET);
	OAuthTwitterConnection2(_api.connection).setRequestToken(requestToken.key, requestToken.secret);
	_api.connection.addEventListener(OAuthTwitterEvent.AUTHORIZED, function(e:OAuthTwitterEvent):void{
		_api.connection.removeEventListener(OAuthTwitterEvent.AUTHORIZED, arguments.callee);
		var accessToken:OAuthToken = _api.connection.accessToken;
		SOManager.getInstance().accessToken = new OAuthToken(accessToken.key, accessToken.secret);
		dispatchEvent(new OAuthTwitterEvent(OAuthTwitterEvent.AUTHORIZED));
	});
	//認証証明からアクセストークンの発行を依頼
	_api.connection.grantAccess(verifier);
}

この記事の最初のほうにも書きましたように、このアクセストークンさえ得られれば、これはユーザーが連係を解除しない限りは永久に有効なトークンなのでアプリ内に保存しておけばOKです。
これでOAuthの認証は完了です。これ以降、アプリ側が認証を求めることはありません。


APIを通じてツイートしてみる

アプリの認証が終わったので、いよいよTwitterのAPIを叩いてみたいと思います。

あれです。いよいよです。自分のアプリから堂々と「てすとなう。」とつぶやくのです。あれです。あれ格好良いですよね!

タイムラインにいるほかのみんなが「うんこなう」とかアホみたいなことつぶやいてたり、食ったモンのインスタグラムをアップしていたりするのを尻目に、ひたすら「てすとなう。」
これです。孤高の開発者っぽい雰囲気がでてて最高にクールです。さっそくやってみたいと思います。

ここで苦労した点は、使用しているライブラリtwitter-actionscript-apiが、APIのバージョン1にしか対応していなかった点でした。
現在TwitterAPIの最新バージョンは1.1で、一世代前であるバージョン1はもうすぐ終了することがアナウンスされています。

そこで使用ライブラリtwitter-actionscript-apiもAPI1.1に対応させる必要があるのですが、その手法を見つけるまでに、だいぶいろいろ試して時間がかかりました。
見つかるまでは時間がかかりましたが、見つかってしまえば変更は簡単です。さっそくやってみたいと思います。

まず前知識として、twitter-actionscript-apiではAPIを叩くためにcom.dborisenko.api.twitter.net.TwitterOperationというクラスを基本としています。
このTwitterOperationクラスはcom.dborisenko.api.HttpOperationといってHTTPベースのAPIを叩くのに便利なクラスのサブクラスとなっていて、TwitterのAPIを使用するのに適したクラスです。

で、このTwitterOperationをスーパークラスとしてさらにツリー上に細かくサブクラスが派生するわけですが、今回試すツイートを投稿するクラスはこちら。

com.dborisenko.api.twitter.commands.status.UpdateStatus

これをAPI1.1用にしないといけないので、次のようなクラスを新たに作りました。

UpdateStatus2
package twitter
{
	import com.dborisenko.api.enums.ResultFormat;
	
	public class UpdateStatus2 extends StatusOperation2
	{

		protected static const URL:String = "https://api.twitter.com/1.1/statuses/update.json";
		
		public function UpdateStatus2(statusText:String, inReplyToStatusId:String=null)
		{
			super(URL);
			resultFormat = ResultFormat.JSON;
			method = METHOD_POST;
			_requiresAuthentication = true;
			_apiRateLimited = false;
			
			statusText = statusText.replace(/\r\n/g, " ");
			statusText = statusText.replace(/\n/g, " ");
			statusText = statusText.replace(/\r/g, " ");
			
			parameters = {status: statusText, in_reply_to_status_id: inReplyToStatusId};
		}

	}
}

もとのUpdateStatusと比べて変更してあるところは、7行目

protected static const URL:String = "https://api.twitter.com/1.1/statuses/update.json";

のURLのところと、12行目

resultFormat = ResultFormat.JSON;

APIが返す形式をJSONに指定しているところだけです。
URLの変更は当然ですが、返す値の形式が1のときはXMLとJSONだったのに対して1.1ではJSONのみになったためです。

次に、UpdateStatusの親クラスであるStatusOperationクラスが、返す形式をXMLしか想定していないのでJSONを扱うように変更します。

StatusOperation2
package twitter
{
	import com.dborisenko.api.enums.ResultFormat;
	import com.dborisenko.api.twitter.data.TwitterStatus;
	import com.dborisenko.api.twitter.net.TwitterOperation;
	
	import flash.events.Event;
	
	public class StatusOperation2 extends TwitterOperation
	{
		private var isMention:Boolean = false;
		
		public function StatusOperation2(url:String, requiresAuthentication:Boolean=true, params:Object=null, isMentions:Boolean=false)
		{
			super(url, requiresAuthentication, params);
		}
		
		[Bindable]
		public function get status():TwitterStatus
		{
			return data as TwitterStatus;
		}
		public function set status(value:TwitterStatus):void
		{
			data = value;
		}
		
		override protected function handleResult(event:Event):void
		{
			switch (resultFormat){
				case ResultFormat.JSON:
					var json:Object = getJSON();
					status = new TwitterStatus(json);
					break;
				case ResultFormat.XML:
					var xml:XML = getXML();
					if (xml.name() == "status")
					{
						status = new TwitterStatus(xml);
					}
					break;
			}
			super.handleResult(event);
		}
	}
}

これでライブラリの変更は完了です。
他のAPIも、同じ変更だけでAPI1.1に対応できると思いまーす。試してませんが、まあ言い切って大丈夫でしょう。

あとは、おもむろに「てすとなう。」とつぶやくのです。

var text:String = 'てすとなう。';
var d:Date = new Date();
var dateStr:String = d.fullYear + '/' + String((d.month + 1)) + '/' + d.date + ' ' + d.hours + ':' + d.minutes + ':' + d.seconds + '.' + d.milliseconds;
var op:TwitterOperation = new UpdateStatus2(text + dateStr);
_api.post(op);

4行目と5行目、みましたか。
TwitterOperationクラスの派生クラスのインスタンスをTwitterAPIのpostメソッドに引数として渡すだけで、APIがたたけます。これわ便利!!!

ここで私が陥ったのが、Twitterは同じツイートを連続して投稿できないという仕様を忘れていたことです。
1行目から3行目まで、ツイートに時間をつけて投稿していますが、最初この考えまで至らず、ずっと「てすとなう。」だけをツイートしようとしていました。
最初に一回だけツイートできた後は、ずっと原因不明のエラーになってて、「もしかしたらAPI1.1対応したどこかで間違ってた?」と思ってライブラリの中身のソースをああでもないこうでもないと読みあさってみたりしていました。結果的にライブラリに対しての理解が深まったのでいいのですが、ここでずいぶんと時間をとってしまい苦労しました。


画像付きでツイートできるようにする

はれてAIRでツイートできるようになったので、せっかくなので画像もいっしょに投稿したいです。
ビジュアル表現にめっぽう強いAS3。
そのAS3でツイートするんだからなんかバーンと格好良い画像をタイムラインのみなさんにお披露目してやってください。

しかしながらtwitter-actionscript-apiには画像付きでツイートするオペレーションは無いので、自分で作る必要があります。

まずは公式ドキュメントPOST statuses/update_with_mediaをみて、仕様を理解します。

画像付きでツイートするには、通常のツイートPOST statuses/updateに比べてmedia[]というパラメーターに画像を追加してPOST statuses/update_with_mediaをコールすればいいそうです。
そのとき、Content-Typeはmultipart/form-dataにしなさいよ、と。ふむふむ。

multipart/form-dataをAS3で作る方法は、きんくまデザインさんの記事「[AS3] PHPとAS3の連携 multipart/form-dataでデータのアップロード」が非常に参考になりました。たいへん有意義な情報ありがとうございます。

実際にクラスを書いてみたいと思います。

UpdateStatusWithMedia
/** UpdateStatusWithMedia.as
 * 
 * @author Masamune Utsunomiya
 */
package twitter
{
	import com.dborisenko.api.enums.ResultFormat;
	
	import flash.display.BitmapData;
	import flash.net.URLRequestHeader;
	import flash.utils.ByteArray;
	
	import mx.graphics.codec.JPEGEncoder;
	import mx.graphics.codec.PNGEncoder;
	import mx.rpc.events.FaultEvent;
	import mx.rpc.events.ResultEvent;
	
	/**
	 * Updates the authenticating user's status.  Requires the status parameter specified below.  Request must be a POST.  
	 * A status update with text identical to the authenticating user's current status will be ignored to prevent 
	 * duplicates.
	 * 
	 * @see https://dev.twitter.com/docs/api/1.1/post/statuses/update_with_media
	 */
	public class UpdateStatusWithMedia extends StatusOperation2
	{
		/**
		 * @private
		 */
		protected static const URL:String = "https://api.twitter.com/1.1/statuses/update_with_media.json";
		
		public static const BOUNDARY:String = 'bbbboooouuuunnnnddddaaaarrrryyyy';
		private static const _DELIMITER:String = '--';
		
		public function UpdateStatusWithMedia(statusText:String, media:ByteArray, inReplyToStatusId:String=null)
		{
			super(URL);
			resultFormat = ResultFormat.JSON;
			method = METHOD_POST;
			_requiresAuthentication = true;
			_apiRateLimited = false;
			
			statusText = statusText.replace(/\r\n/g, " ");
			statusText = statusText.replace(/\n/g, " ");
			statusText = statusText.replace(/\r/g, " ");
			
			parameters = {status: statusText, in_reply_to_status_id: inReplyToStatusId};
			parameters['media[]'] = media;
		}
		
		override public function execute():void
		{
			initOperation();
			serviceDelegate = new HTTPServiceDelegateForMultipart();
			serviceDelegate.headers = headers;
			serviceDelegate.method = method;
			serviceDelegate.params = params;
			serviceDelegate.resultFormat = resultFormat;
			serviceDelegate.url = url;
			
			serviceDelegate.addEventListener(ResultEvent.RESULT, handleResult);
			serviceDelegate.addEventListener(FaultEvent.FAULT, handleFault);
			serviceDelegate.send();
		}
		
		override protected function initOperation():void
		{
			if (requiresAuthentication && twitterAPI.connection != null)
			{
				var oauthParams:Object = params;
				var newUrl:String = url;
				
				var headerParams:Object = {};
				
				for (var key:String in oauthParams)
				{
					headerParams[key] = oauthParams[key];
				}
				
				var header:URLRequestHeader = twitterAPI.connection.createOAuthHeader(
					method, newUrl, {});
				if (header)
				{
					var hName:String = header.name;
					var hValue:String = header.value;
					headers[hName] = hValue;
				}
			}
		}

		private static function _addBoundary():String
		{
			return _DELIMITER + BOUNDARY;
		}
		
		private static function _addData(d:ByteArray, key:String, value:*):ByteArray
		{
			d.writeUTFBytes("\r\n");
			if(value is ByteArray){
				d.writeUTFBytes('Content-Type: application/octet-stream');
				d.writeUTFBytes("\r\n");
			}
			d.writeUTFBytes('Content-Disposition: form-data; name="' + key + '"');
			if(value is ByteArray){
				d.writeUTFBytes(';');
			}
			d.writeUTFBytes("\r\n\r\n");
			if(value is ByteArray){
				d.writeBytes(value);
			}else{
				d.writeUTFBytes(value);
			}
			d.writeUTFBytes("\r\n");
			d.writeUTFBytes(_addBoundary());
			return d;
		}
		
		public static function buildData(params:Object):ByteArray
		{
			var b:ByteArray = new ByteArray();
			b.writeUTFBytes(_addBoundary());
			for(var key:String in params){
				b = _addData(b, key, params[key]);
			}
			b.writeUTFBytes(_DELIMITER);
			return b;
		}
		
		public static function jpeg(bitmapData:BitmapData):ByteArray
		{
			var e:JPEGEncoder = new JPEGEncoder(80);
			return e.encode(bitmapData);
		}
		
		public static function png(bitmapData:BitmapData):ByteArray
		{
			var e:PNGEncoder = new PNGEncoder();
			return e.encode(bitmapData);
		}
	}
}

multipart/form-dataの作成は、フォーマット整えるだけなので簡単だったのですが、苦労した点はOAuthHeaderの生成をどうするかというところでした。

というのも、TwitterAPIのオペレーションで核となるクラス、TwitterOperationではinitOperation()メソッドでOAuthHeaderというものを生成しています。
このOAuthHeaderの中には、クライアントから渡されたパラメーターが途中で改ざんされていないかをチェックするためにハッシュ値を照合する、「oauth_signature」という値が含まれています。ライブラリでいうとorg.iotashan.oauth.OAuthRequestの141行目付近、

// generate the signature
var signature:String = signatureMethod.signRequest(this);
_requestParams["oauth_signature"] = signature;

このあたりで作られています。

で、私は不勉強ながらmultipart/form-dataで渡すとき、そもそもこのパラメーターをどうしよう?というのがわからなかったので、正解を得るまでにものすごく時間がかかってしまいました。

普通に&繋がりのKEY=VALUE形式で渡してみるか?Objectを渡したら中でそのように変換してるな?そのときmedia[]の値はバイナリでいいのか?Base64なり文字列に直してみるか?そもそもmedia[]は取り除いてstatusだけでやってみるか?などなど、一晩中かかっていろいろ試していたのを覚えています。

いよいよ万策尽きてほぼあきらめた気持ちでいったん仮眠をとり、ものは試しにパラメーターとして{}←空のObjectを渡したときのことです。
それまでの苦労が何だったのかと思えるくらい、あっけなく投稿が成功したのです。

何のことはない、multipart/form-dataで送るときはOAuthHeaderを生成するときのパラメーターは空でよかったわけです。
PHPやJavaやRubyなど他の言語のサンプルを探してみても、ここらへんの処理で特別なことをしているふうでもなく本当に困っていました。今思えばパラメーターは空でいいので、それこそ特別何もしない、という正解はすでに得られていたはずなんですが、そのときは目の前にある正解に気付くことはありませんでした。多言語のサンプルもAPIのバージョンが1のときのが多く、1.1についての情報が少なかったのも迷ったポイントでした。

とにもかくにも迷いながらも最終的には答えにたどり着き、成功したのです。Twitter界で空気だったAS3で。他の言語でもまだそんなに詳しくやってないAPI1.1で。これにはものすごい充実感をおぼえました。
実際にはほかのみんなはAPI1から特筆するほど変更点がなくすんなり移行できたから、こんだけ苦労してるのは私しかいないから情報が少ないだけなんですけど。

あとはこんな感じでAPIを叩くと、画像付きでツイートできます。

//ツイートする文字
var text:String = 'てすとなう。';
//連投禁止仕様回避のため、時刻をつけてつぶやく
var d:Date = new Date();
var dateStr:String = d.fullYear + '/' + String((d.month + 1)) + '/' + d.date + ' ' + d.hours + ':' + d.minutes + ':' + d.seconds + '.' + d.milliseconds;
//ツイートに添える格好良い画像を生成!
var sp:Sprite = new Sprite();
sp.graphics.beginFill(0xFFFF00);
sp.graphics.drawRect(0, 0, 300, 200);
sp.graphics.endFill();
var t:TextField = new TextField();
t.text = 'てすとなうなう。';
t.x = (sp.width - t.width) * 0.5;
t.y = (sp.height - t.height) * 0.5;
sp.addChild(t);
var bitmapData:BitmapData = new BitmapData(300, 200);
bitmapData.draw(sp);
var byte:ByteArray = UpdateStatusWithMedia.jpeg(bitmapData);
//画像付きでツイート
var op:TwitterOperation = new UpdateStatusWithMedia(text + dateStr, byte);
op.addEventListener(TwitterEvent.COMPLETE, function(e:TwitterEvent):void{
	trace(e.success);
	trace(e.data.toString());
});
_api.post(op);

やった。ついにやったよ!


まとめ

というわけでみんなもAIRでAS3でツイッターしよー。

コメントを残す

メールアドレスが公開されることはありません。