だらだらやるよ。

こげつのIT技術メモ

Twitterクライアント作ってます。

だいぶのびちゃったけど今月中には公開する予定。
接続用クラスとかパーサ書いたから張っておこう
パーサはとりあえずXPath使わずに書いてみた。パフォーマンス見てXPathのが良かったら切り替える予定。

TwitterConnector.cs

using System;

using System.Collections.Generic;
using System.Text;
using System.Net;
using System.IO;

namespace Smartter
{
	class TwitterConnector
	{
		string _id = "";
		string _pass = "";
		string _clientName = "";
		
		public string Id {
			get { return _id; }
			set { _id = value; }
		}
		
		public string Pass {
			get { return _pass; }
			set { _pass = value; }
		}
		public TwitterConnector(string id,string pass) {
			_id = id;
			_pass = pass;
		}
		public TwitterConnector(string id,string pass,string clientName):this(id,pass) {
			_clientName = clientName;
		}
		/// <summary>
		/// 接続できるか確認する。
		/// </summary>
		/// <returns></returns>
		public bool CanConnect() {
			WebRequest req = CreateWebRequest("http://twitter.com/account/rate_limit_status.xml");
			req.Method = "GET";
			try {
				HttpWebResponse wr = (HttpWebResponse)req.GetResponse();
				HttpStatusCode ht = wr.StatusCode;
				wr.Close();
				return ht == HttpStatusCode.OK;
			} catch {
				return false;
			}
		}


		public bool GetPublicTimeline(Action<string> xmlParseAction) {
			try {
				Get("http://twitter.com/statuses/friends_timeline.xml",xmlParseAction);
			} catch {
				return false;
			}
			return true;
		}
		public bool GetReplies(Action<string> xmlParseAction) {
			try {
				Get("http://twitter.com/statuses/replies.xml",xmlParseAction);
			} catch {
				return false;
			}
			return true;
		}

		public bool GetDirectMessage(Action<string> xmlParseAction) {
			try {
				Get("http://twitter.com/direct_messages.xml",xmlParseAction);
			} catch {
				return false;
			}
			return true;
		}
	

		/// <summary>
		/// アドレスにアクセスし、取得したデータに対して処理を行う
		/// </summary>
		/// <param name="uri"></param>
		/// <param name="act"></param>
		private void Get(string uri,Action<string> xmlParseAction) {
			WebRequest req = CreateWebRequest(uri);
			req.Method = "GET";
			req.BeginGetResponse(delegate(IAsyncResult r) {
				WebResponse rps = req.EndGetResponse(r);
				using(StreamReader sr = new StreamReader(rps.GetResponseStream(),Encoding.UTF8)) {
					xmlParseAction(sr.ReadToEnd());
				}
			},req);
		}



		/// <summary>
		/// 発言する
		/// </summary>
		/// <param name="status">発言内容</param>
		/// <returns>結果</returns>
		public bool UpdateStatus(string status) {
			byte[] postValue = CreatePostValue(status);
			WebRequest req = CreateWebRequest("http://twitter.com/statuses/update.xml");
			req.Method = "POST";
			req.ContentLength = postValue.Length;

			try {
				using(Stream strm = req.GetRequestStream()) {
					strm.Write(postValue,0,postValue.Length);
				}
			} catch(Exception ex) {
				Logger.Write(ex.Message);
				//接続エラー
				return false;
			}
			HttpWebResponse result = null;
			try {
				result = (HttpWebResponse)req.GetResponse();
				return result.StatusCode == HttpStatusCode.OK;
			} catch(WebException ex) {
				//認証のエラー
				Logger.Write(ex.Message);
				return false;
			} catch(Exception ex) {
				//それ以外のエラー
				Logger.Write(ex.Message);
				return false;
			}
		}


		/// <summary>
		/// urlからリクエストを作成する
		/// </summary>
		/// <param name="uri"></param>
		/// <returns></returns>
		private WebRequest CreateWebRequest(string uri) {
			WebRequest req = HttpWebRequest.Create(uri);
			req.ContentType = "application/x-www-form-urlencoded";
			req.Headers.Add(CreateAuthString());
			return req;
		}
		/// <summary>
		/// Basic認証用の文字列を作成する
		/// </summary>
		/// <returns></returns>
		private string CreateAuthString() {
			return "Authorization: Basic " + Convert.ToBase64String(Encoding.UTF8.GetBytes(_id + ":" + _pass));
		}
		/// <summary>
		/// 発言する内容を作成する
		/// </summary>
		/// <param name="status"></param>
		/// <returns></returns>
		private byte[] CreatePostValue(string status) {
			return Encoding.ASCII.GetBytes("source=" + Uri.EscapeUriString(_clientName) + "&status=" + Uri.EscapeUriString(status));
		}
	}
}


TwitterXmlParser.cs

using System;

using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Xml;

namespace Smartter
{
	static class TwitterXmlParser
	{
		public delegate void Parse(string xmlString,Action<string[]> lineAction);
		
		
		/// <summary>
		/// タイムラインを解析して1行ごとに処理を行う
		/// </summary>
		/// <param name="s"></param>
		/// <param name="lineAct"></param>
		internal static void ParseTimeLine(string s,Action<string[]> lineAction) {
			StringReader sr = new StringReader(s);
			XmlTextReader xtr = new XmlTextReader(sr);
			while(xtr.Read()) {
				if(!IsStatusElement(xtr)) {
					continue;
				}
				lineAction(ReadStatusData(xtr));
			}
			xtr.Close();
			sr.Close();
		}
		/// <summary>
		/// 一つの発言(<status>から</status>まで)を解析する
		/// </summary>
		/// <param name="xtr"></param>
		/// <returns>解析結果の文字列配列</returns>
		private static string[] ReadStatusData(XmlTextReader xtr) {
			//1つの発言を解析
			//ID = 0,
			//Name = 1,
			//ScreenName = 2,
			//PostData = 3,
			//CreateAt = 4
			//UserID = 5
			String[] s = new string[6];
			while(xtr.Read()) {
				if(xtr.NodeType == XmlNodeType.EndElement && xtr.Name == "status") {
					return s;
				}
				if(xtr.NodeType != XmlNodeType.Element) { continue; }
				switch(xtr.Name) {
					case "created_at"://発言時間
						s[4] = GetNextValue(xtr);
						break;
					case "id"://POST番号
						s[0] = GetNextValue(xtr);
						break;
					case "text"://post
						s[3] = GetNextValue(xtr);
						break;
					case "user":
						while(xtr.Read()) {
							if(xtr.NodeType == XmlNodeType.EndElement && xtr.Name == "user") {
								break;
							}
							if(xtr.NodeType != XmlNodeType.Element) { continue; }
							switch(xtr.Name) {
								case "id"://userid
									s[5] = GetNextValue(xtr);
									break;
								case "name"://表示名
									s[1] = GetNextValue(xtr);
									break;
								case "screen_name"://アカウント
									s[2] = GetNextValue(xtr);
									break;

							}
						}
						break;
				}
			}
			//ここにはこないはず
			return s;
		}
		/// <summary>
		/// 一つ読んで次の値を返す
		/// </summary>
		/// <param name="xtr"></param>
		/// <returns></returns>
		private static string GetNextValue(XmlTextReader xtr) {
			xtr.Read();
			return xtr.Value;
		}

		/// <summary>
		/// Statusエレメントか調査する
		/// </summary>
		/// <param name="xtr"></param>
		/// <returns></returns>
		private static bool IsStatusElement(XmlTextReader xtr) {
			return xtr.NodeType == XmlNodeType.Element && xtr.Name == "status";
		}

		/// <summary>
		/// メッセージを解析してメッセージを配列に
		/// </summary>
		/// <param name="s"></param>
		/// <param name="lineAct"></param>
		internal static void ParseDirectMessage(string s,Action<string[]> lineAct) {
			StringReader sr = new StringReader(s);
			XmlTextReader xtr = new XmlTextReader(sr);
			while(xtr.Read()) {
				if(xtr.NodeType == XmlNodeType.Element || xtr.Name != "direct_message") {
					continue;
				}
				lineAct(ReadDirectMessageData(xtr));
			}
			xtr.Close();
			sr.Close();
		}
		private static string[] ReadDirectMessageData(XmlTextReader xtr) {
			//ID = 0,
			//Name = 1,
			//ScreenName = 2,
			//DmData = 3,
			//CreateAt = 4
			//SenderUserID = 5
			String[] s = new string[6];
			while(xtr.Read()) {
				if(xtr.NodeType == XmlNodeType.EndElement && xtr.Name == "direct_message") {
					return s;
				}
				if(xtr.NodeType != XmlNodeType.Element) { continue; }
				switch(xtr.Name) {
					case "created_at"://発言時間
						s[4] = GetNextValue(xtr);
						break;
					case "sender_id"://POST番号
						s[5] = GetNextValue(xtr);
						break;
					case "id"://POST番号
						s[0] = GetNextValue(xtr);
						break;
					case "text"://post
						s[3] = GetNextValue(xtr);
						break;
					case "sender":
						while(xtr.Read()) {
							if(xtr.NodeType == XmlNodeType.EndElement && xtr.Name == "sender") {
								break;
							}
							if(xtr.NodeType != XmlNodeType.Element) { continue; }
							switch(xtr.Name) {
								case "name"://表示名
									s[1] = GetNextValue(xtr);
									break;
								case "screen_name"://アカウント
									s[2] = GetNextValue(xtr);
									break;

							}
						}
						return s;
				}

			}
			//ここにはこないはず
			return s;
		}
	}
}


呼び出すときはこんな感じ。

		private void ReadTwitterData() {
TwitterConnector t = new TwitterConnector("id","pass","appName");
			t.GetPublicTimeline(delegate(string st) {
				Invoke(CreateParseAction(lvPublicTimeline,TwitterXmlParser.ParseTimeLine),st);
			});
			t.GetReplies(delegate(string st) {
				Invoke(CreateParseAction(lvRep,TwitterXmlParser.ParseTimeLine),st);
			});

			t.GetDirectMessage(delegate(string st) {
				Invoke(CreateParseAction(lvDm,TwitterXmlParser.ParseDirectMessage),st);
			});
		}

		private static Action<string> CreateParseAction(ListView lv,TwitterXmlParser.Parse parse) {
			int saigo = lv.Items.Count == 0 ? 0 : Convert.ToInt32(lv.Items[0].Text);
			Action<string> mainAction = delegate(string s) {
				lv.BeginUpdate();
				parse(s,delegate(string[] ss) {
					if(saigo < Convert.ToInt32(ss[0])) {
						lv.Items.Add(new ListViewItem(ss));
					}
				});
				lv.EndUpdate();
			};
			return mainAction;
		}

デリゲートつかってるから、若干見づらいかもしれないけど
接続部分、XMLの解析、表示部分のコードを完全にわけたかったのと、非同期実行したかったのでこんな感じになりました。
メソッド名とか変数名はリファクタリングの必要あるし、毎回idとpassもって認証しにいってるので、クッキー持ちまわせる幹事に変更はしたいかなぁ