Adobe AIRでTwitterクライアントを作る その2

前回作ったTwitterクライアントはタイムラインの取得しか出来なかったので今回は発言の投稿が出来るように改良しました。

元のコードは以下のページを参照。
【連載】今からはじめるAIRプログラミング(13) HTTPリクエスト用API - Web上のサービスをローカルアプリに取り込む

ActionScriptでPOSTするときに

var uv:URLVariables = new URLVariables();
uv.decode("status="+" 発言 ");

とやってデコードしなきゃいけないことに気づくのに1週間ほどかかりましたよ・・・。


以下、ソースです。やたらと長いのでご注意ください。
後、
「以下のソースを利用する事で被害を被ったとしても私 Mine はなんら関与しないものとします。」
ということで。

やってる処理についてはコメントを参照してください。


以下、AIRTwitterClient.mxml。処理のメインです。

<?xml version="1.0" encoding="utf-8"?>
<mx:WindowedApplication xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" creationComplete="init()" width="454" height="300">
	<mx:Metadata>
    	//メタデータ。"onLoginSuccess"イベントを発行する可能性があることを示す
        [Event("onLoginSuccess")]
    </mx:Metadata>
	
	<mx:Script>
		<![CDATA[
			// 配列を利用する為のインポート
            import mx.collections.ArrayCollection;
            // ポップアップを利用する為のインポート
            import mx.managers.PopUpManager;
            // アラートを利用する為のインポート
            import mx.controls.Alert;
			
			// ログイン成功イベントを表す文字列
            public static const ON_LOGIN_SUCCESS:String = "onLoginSuccess";

            // ログインダイアログ
            private var loginDialog:LoginDialog;

			private var userName:String = "";
			private var password:String = "";
			
			//RFC2822形式準拠の日時を生成する為に使う
			private var weekDayLabels:Array = new Array("Sun",
				"Mon",
            	"Tue",
            	"Wed",
            	"Thu",
            	"Fri",
            	"Sat");
			private var monthLabels:Array = new Array("Jan",
            	"Feb",
            	"Mar",
           		"Apr",
            	"May",
            	"Jun",
            	"Jul",
             	"Aug",
              	"Sep",
              	"Oct",
              	"Nov",
              	"Dec");
			private var date:Date;
			
            // データグリッドと連動するコレクション
            [Bindable]
            private var timeline:ArrayCollection = new ArrayCollection();

            // 初期化
            private function init():void {
                // ログインダイアログの作成
                loginDialog = PopUpManager.createPopUp(this, LoginDialog, true) as LoginDialog;
                // ログイン成功時のイベントリスナを追加
                loginDialog.addEventListener(LoginDialog.ON_LOGIN_SUCCESS, onLoginSuccess);
                // ダイアログを中央に表示
                PopUpManager.centerPopUp(loginDialog);
            }
            
            
            // ログイン成功時の処理
            private function onLoginSuccess(e:Event):void {
        		if(userName != ""){
        			//初回呼び出しのときだけユーザー名とパスワードを取得
            		userName=loginDialog.userName.text;
            		password=loginDialog.password.text;
          		}
                // ポップアップを除去
                PopUpManager.removePopUp(loginDialog);

                // 入力されたユーザ名(ログイン失敗を考慮していない)
                var userName:String = loginDialog.userName.text;
                
                // リクエスト用文字列を生成
				var requestString:String = "http://twitter.com/statuses/friends_timeline/" + userName + ".xml";
				
				// 時刻が以前更新されていれば、それをもとにする。
				if(date!=null){
					/*
					//前回から更新されたところだけを取得しようという部分。このせいでIOエラーになる。
					var since:String = "?since=" 
						+ weekDayLabels[date.getDay()] 
						+ "%2C+" + date.getDate() + "+" 
						+ monthLabels[date.getMonth()] + "+" 
						+ date.getFullYear() + "+" 
						+ date.getHours() + "%3A" 
						+ date.getMinutes() + "%3A" 
						+ date.getSeconds() + "+GMT";
					requestString = requestString + since;
					*/
					// 今のところタイムラインを全部書き換えている。
					timeline.removeAll();
				}
				
                // URLRequestを用いてTwitterのデータを取得(直近20件に限定)
                var req:URLRequest = new URLRequest(requestString);
				
                // 読み込みが完了した時刻を設定
                date = new Date();
				
                // URLLoaderで読み込み
                var loader:URLLoader = new URLLoader(req);
                
                // 読み込み完了時の処理
                loader.addEventListener(Event.COMPLETE, function(event:Event):void {
                	//XML処理
                    var responseXML:XML = new XML(loader.data);
                    var index:int = 0;
                    //javaの拡張for文。responseXML.children()がなくなるまで、子をstatusに代入して回す。20回回るはず。
                    for each (var status:XML in responseXML.children()) {
                    	
                    	//タイムラインにアイテムを追加
                        timeline.addItem({
                        	//画像。urlを指定してダウンロードしている。
                            image: status.user.profile_image_url,
                            //名前。xml文書が持つ要素。
                            name: status.user.name,
                            //テキスト。ここでは発言の内容。これもxml文書に埋め込まれている。
                            text: status.text
                		}//,
                		//index
                		);
                		//index++;
                    }
                });
                
				
            }
            
            
            // 更新ボタンが押されたときの反応
            private function buttonClicked():void {
            	
            	// 自分自身にリスナを登録。
            	this.addEventListener(LoginDialog.ON_LOGIN_SUCCESS, onLoginSuccess);
            	
            	// テキストフィールドが空か、そうでないか
            	if(speak.text == ""){
            		sendMessage("");
            	}else{
            		var sendmessage:String = speak.text;
            		speak.text = "";
            		
            		sendMessage(sendmessage);
            	}
            }
            
            // 発言を登録する
            private function sendMessage(message:String):void {
            	
            	// 以降のURLRequestが全て認証情報付きで行われるように、デフォルト値としてセット
            	URLRequestDefaults.setLoginCredentialsForHost("twitter.com", userName, password);
				
            	// 以降、TwitterにHTTPリクエストを送信する処理
            	var req:URLRequest = new URLRequest("http://twitter.com/statuses/update.xml");
            	// POSTメソッドでなければいけない
            	req.method = "POST";
				
				// URIエンコードする
            	if(message.length > 0){
            		var uv:URLVariables = new URLVariables();
            		
            		// @fixme 英単語の直後にくる文字列が欠落してしまう。 例) "twitterクライアント" → "twitter"
            		uv.decode("status="+message);
            		req.data = uv;
            	}
            	
            	// HTTPリクエストを生成。
            	var loader:URLLoader = new URLLoader();
			
            	// HTTPレスポンスイベントを捕捉
            	loader.addEventListener(HTTPStatusEvent.HTTP_RESPONSE_STATUS, onHTTPResponse);
            
            	// HTTPリクエストを実行。
            	loader.load(req);
            	
            }
            
            // twitter.comから応答が得られたときの動作
            private function onHTTPResponse(event:HTTPStatusEvent):void {
        	
            	// HTTPレスポンスステータスが401(Unauthorized)だった場合、アラートを表示
            	if (event.status == 401) {
            	    Alert.show("Error:401", "エラー");
            	    return;
            	}
            	
            	// イベントを送出
            	dispatchEvent(new Event(ON_LOGIN_SUCCESS));
            
            }
        
        ]]>
        <!--スクリプトの終端-->
    </mx:Script>
        
    <!--データグリッド。データを格子状に並べる。名前は"timeline"-->
    <mx:DataGrid dataProvider="{timeline}" right="0" bottom="60" top="0" left="0">
        <!--列の成分を指定-->
        <mx:columns>
    		<!--イメージのレンダリング先を指定-->
            <mx:DataGridColumn headerText="画像" dataField="image" width="40">
                <mx:itemRenderer>
                    <mx:Component><mx:Image width="32" height="32"/></mx:Component>
                </mx:itemRenderer>
            </mx:DataGridColumn>
            <!--ユーザ名-->
            <mx:DataGridColumn headerText="ユーザ名" dataField="name" width="120"/>
            <!--コメント(発言)-->
            <mx:DataGridColumn headerText="コメント" dataField="text" wordWrap="true"/>
    	</mx:columns>
           	
    </mx:DataGrid>
    
    <mx:TextArea height="33" wordWrap="true" editable="true" bottom="19" left="0" right="54" id="speak"/>
    <mx:Button label="更新" right="0" bottom="31" click="buttonClicked();"/>
    
</mx:WindowedApplication>


以下、LoginDialog.mxml。ログインダイアログです。

<?xml version="1.0" encoding="utf-8"?>
<mx:TitleWindow xmlns:mx="http://www.adobe.com/2006/mxml" width="320" height="200" title="ログイン">
    <mx:Metadata>
    	//メタデータ。"onLoginSuccess"イベントを発行する可能性があることを示す
        [Event("onLoginSuccess")]
    </mx:Metadata>
    <mx:Script>
        <![CDATA[
        	//ポップアップを使う為のインポート
            import mx.managers.PopUpManager;
            //アラートを使う為のインポート
            import mx.controls.Alert;

            // ログイン成功イベントを表す文字列
            public static const ON_LOGIN_SUCCESS:String = "onLoginSuccess";

            // ログインボタン押下字の処理
            private function login():void {
                // 以降のURLRequestが全て認証情報付きで行われるように、デフォルト値としてセット
                URLRequestDefaults.setLoginCredentialsForHost("twitter.com", userName.text, password.text);
				
                // 以降、TwitterにHTTPリクエストを送信する処理
                var req:URLRequest = new URLRequest("http://twitter.com/statuses/update.xml");
                // POSTメソッドでなければいけない
                req.method = "POST";
				
                // Twitterは明示的に認証を行うためのAPIが提供されていないので、
                // 長さゼロの文字列でステータスを変更してみることによって代用
                var variables : URLVariables = new URLVariables ();
                variables.status = "";
				
                // HTTPリクエストを生成。
                var loader:URLLoader = new URLLoader();
				
                // HTTPレスポンスイベントを捕捉
                loader.addEventListener(HTTPStatusEvent.HTTP_RESPONSE_STATUS, onHTTPResponse);
                
                // HTTPリクエストを実行。
                loader.load(req);
				
				//trace("request!");
				
            }
            
            //
            private function onHTTPResponse(event:HTTPStatusEvent):void {
            	
                // HTTPレスポンスステータスが401(Unauthorized)だった場合、アラートを表示
                if (event.status == 401) {
                    Alert.show("ログインできません。", "エラー");
                    return;
                }
                
                // イベントを送出
                dispatchEvent(new Event(ON_LOGIN_SUCCESS));
            }
            
        ]]>
    </mx:Script>
    <mx:Label text="ユーザ名とパスワードを入力してログインしてください。" fontWeight="bold"/>
    <mx:Form width="100%" height="85" y="21">
        <mx:FormItem label="ユーザ名">
            <mx:TextInput id="userName"/>
        </mx:FormItem>
        <mx:FormItem label="パスワード">
            <mx:TextInput id="password" displayAsPassword="true"/>
        </mx:FormItem>
    </mx:Form>
    <mx:Button label="ログイン" x="203" y="101" click="login()"/>
</mx:TitleWindow>


このファイルは特に変更無し。
AIRTwitterClient-app.xml

<?xml version="1.0" encoding="UTF-8"?>
<application xmlns="http://ns.adobe.com/air/application/1.0">

<!-- Adobe AIR Application Descriptor File Template.

	Specifies parameters for identifying, installing, and launching AIR applications.
	See http://www.adobe.com/go/air_1.0_application_descriptor for complete documentation.

	xmlns - The Adobe AIR namespace: http://ns.adobe.com/air/application/1.0
			The last segment of the namespace specifies the version 
			of the AIR runtime required for this application to run.
			
	minimumPatchLevel - The minimum patch level of the AIR runtime required to run 
			the application. Optional.
-->

	<!-- The application identifier string, unique to this application. Required. -->
	<id>AIRTwitterClient</id>

	<!-- Used as the filename for the application. Required. -->
	<filename>AIRTwitterClient</filename>

	<!-- The name that is displayed in the AIR application installer. Optional. -->
	<name>AIRTwitterClient</name>

	<!-- An application version designator (such as "v1", "2.5", or "Alpha 1"). Required. -->
	<version>v1</version>

	<!-- Description, displayed in the AIR application installer. Optional. -->
	<!-- <description></description> -->

	<!-- Copyright information. Optional -->
	<!-- <copyright></copyright> -->

	<!-- Settings for the application's initial window. Required. -->
	<initialWindow>
		<!-- The main SWF or HTML file of the application. Required. -->
		<!-- Note: In Flex Builder, the SWF reference is set automatically. -->
		<content>[この値は Flex Builder の出力ファイル app.xml に上書きされます]</content>
		
		<!-- The title of the main window. Optional. -->
		<!-- <title></title> -->

		<!-- The type of system chrome to use (either "standard" or "none"). Optional. Default standard. -->
		<!-- <systemChrome></systemChrome> -->

		<!-- Whether the window is transparent. Only applicable when systemChrome is false. Optional. Default false. -->
		<!-- <transparent></transparent> -->

		<!-- Whether the window is initially visible. Optional. Default false. -->
		<!-- <visible></visible> -->

		<!-- Whether the user can minimize the window. Optional. Default true. -->
		<!-- <minimizable></minimizable> -->

		<!-- Whether the user can maximize the window. Optional. Default true. -->
		<!-- <maximizable></maximizable> -->

		<!-- Whether the user can resize the window. Optional. Default true. -->
		<!-- <resizable></resizable> -->

		<!-- The window's initial width. Optional. -->
		<!-- <width></width> -->

		<!-- The window's initial height. Optional. -->
		<!-- <height></height> -->

		<!-- The window's initial x position. Optional. -->
		<!-- <x></x> -->

		<!-- The window's initial y position. Optional. -->
		<!-- <y></y> -->

		<!-- The window's minimum size, specified as a width/height pair, such as "400 200". Optional. -->
		<!-- <minSize></minSize> -->

		<!-- The window's initial maximum size, specified as a width/height pair, such as "1600 1200". Optional. -->
		<!-- <maxSize></maxSize> -->
	</initialWindow>

	<!-- The subpath of the standard default installation location to use. Optional. -->
	<!-- <installFolder></installFolder> -->

	<!-- The subpath of the Windows Start/Programs menu to use. Optional. -->
	<!-- <programMenuFolder></programMenuFolder> -->

	<!-- The icon the system uses for the application. For at least one resolution,
		 specify the path to a PNG file included in the AIR package. Optional. -->
	<!-- <icon>
		<image16x16></image16x16>
		<image32x32></image32x32>
		<image48x48></image48x48>
		<image128x128></image128x128>
	</icon> -->

	<!-- Whether the application handles the update when a user double-clicks an update version
	of the AIR file (true), or the default AIR application installer handles the update (false).
	Optional. Default false. -->
	<!-- <customUpdateUI></customUpdateUI> -->
	
	<!-- Whether the application can be launched when the user clicks a link in a web browser.
	Optional. Default false. -->
	<!-- <allowBrowserInvocation></allowBrowserInvocation> -->

	<!-- Listing of file types for which the application can register. Optional. -->
	<!-- <fileTypes> -->

		<!-- Defines one file type. Optional. -->
		<!-- <fileType> -->

			<!-- The name that the system displays for the registered file type. Required. -->
			<!-- <name></name> -->

			<!-- The extension to register. Required. -->
			<!-- <extension></extension> -->
			
			<!-- The description of the file type. Optional. -->
			<!-- <description></description> -->
			
			<!-- The MIME type. Optional. -->
			<!-- <contentType></contentType> -->
			
			<!-- The icon to display for the file type. Optional. -->
			<!-- <icon>
				<image16x16></image16x16>
				<image32x32></image32x32>
				<image48x48></image48x48>
				<image128x128></image128x128>
			</icon> -->
			
		<!-- </fileType> -->
	<!-- </fileTypes> -->

</application>


次はこれを読んでもう少し勉強したいと思っている。

Twitter!―Twitter APIガイドブック

Twitter!―Twitter APIガイドブック