using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Net; using System.Text; using System.Threading; using System.Xml; using System.Xml.Serialization; using System.IO.Compression; using System.Linq; namespace Misuzilla.Applications.TwitterIrcGateway { /// /// Twitterへの接続と操作を提供します。 /// public class TwitterService : IDisposable { //private WebClient _webClient; private CredentialCache _credential = new CredentialCache(); private IWebProxy _proxy = WebRequest.DefaultWebProxy; private String _userName; private Boolean _cookieLoginMode = false; private Timer _timer; private Timer _timerDirectMessage; private Timer _timerReplies; private DateTime _lastAccessDirectMessage = DateTime.Now; private Int64 _lastAccessTimelineId = 1; private Int64 _lastAccessRepliesId = 1; private Int64 _lastAccessDirectMessageId = 1; private Boolean _isFirstTime = true; private Boolean _isFirstTimeReplies = true; private Boolean _isFirstTimeDirectMessage = true; private LinkedList _statusBuffer; private LinkedList _repliesBuffer; #region Events /// /// 更新チェック時にエラーが発生した場合に発生します。 /// public event EventHandler CheckError; /// /// タイムラインステータスの更新があった場合に発生します。 /// public event EventHandler TimelineStatusesReceived; /// /// Repliesの更新があった場合に発生します。 /// public event EventHandler RepliesReceived; /// /// ダイレクトメッセージの更新があった場合に発生します。 /// public event EventHandler DirectMessageReceived; #endregion #region Fields /// /// Twitter APIのエンドポイントURLのプレフィックスを取得・設定します。 /// public String ServiceServerPrefix = "http://api.twitter.com/1"; /// /// リクエストのRefererを取得・設定します。 /// public String Referer = "http://twitter.com/home"; /// /// リクエストのクライアント名を取得・設定します。この値はsourceパラメータとして利用されます。 /// public String ClientName = "TwitterIrcGateway"; public String ClientUrl = "http://www.misuzilla.org/dist/net/twitterircgateway/"; public String ClientVersion = typeof(TwitterService).Assembly.GetName().Version.ToString(); #endregion /// /// TwitterService クラスのインスタンスをユーザ名とパスワードで初期化します。 /// /// ユーザー名 /// パスワード [Obsolete] public TwitterService(String userName, String password) { _credential.Add(new Uri(ServiceServerPrefix), "Basic", new NetworkCredential(userName, password)); _userName = userName; Initialize(); } /// /// TwitterService クラスのインスタンスをOAuthを利用する設定で初期化します。 /// /// public TwitterService(String clientKey, String secretKey, TwitterIdentity twitterIdentity) { OAuthClient = new TwitterOAuth(clientKey, secretKey) { Token = twitterIdentity.Token, TokenSecret = twitterIdentity.TokenSecret }; _userName = twitterIdentity.ScreenName; Initialize(); } private void Initialize() { _Counter.Increment(ref _Counter.TwitterService); _timer = new Timer(new TimerCallback(OnTimerCallback), null, Timeout.Infinite, Timeout.Infinite); _timerDirectMessage = new Timer(new TimerCallback(OnTimerCallbackDirectMessage), null, Timeout.Infinite, Timeout.Infinite); _timerReplies = new Timer(new TimerCallback(OnTimerCallbackReplies), null, Timeout.Infinite, Timeout.Infinite); _statusBuffer = new LinkedList(); _repliesBuffer = new LinkedList(); Interval = 90; IntervalDirectMessage = 360; IntervalReplies = 120; BufferSize = 250; EnableCompression = false; FriendsPerPageThreshold = 100; EnableDropProtection = true; POSTFetchMode = false; } ~TwitterService() { //_Counter.Decrement(ref _Counter.TwitterService); Dispose(); } /// /// 接続に利用するプロキシを設定します。 /// public IWebProxy Proxy { get { return _proxy; //return _webClient.Proxy; } set { _proxy = value; //_webClient.Proxy = value; } } /// /// Cookieを利用してログインしてデータにアクセスします。 /// [Obsolete("Cookieログインによるデータ取得は制限されました。POSTFetchModeを利用してください。")] public Boolean CookieLoginMode { get { return _cookieLoginMode; } set { _cookieLoginMode = value; } } /// /// POSTを利用してログインしてデータにアクセスします。 /// [Obsolete("POSTによる取得は廃止されました。")] public Boolean POSTFetchMode { get { return false; } set { } } /// /// 取りこぼし防止を有効にするかどうかを指定します。 /// public Boolean EnableDropProtection { #if HOSTING get; set; #else get; set; #endif } /// /// 内部で重複チェックするためのバッファサイズを指定します。 /// public Int32 BufferSize { get; set; } /// /// タイムラインをチェックする間隔を指定します。 /// public Int32 Interval { get; set; } /// /// ダイレクトメッセージをチェックする間隔を指定します。 /// public Int32 IntervalDirectMessage { get; set; } /// /// Repliesをチェックする間隔を指定します。 /// public Int32 IntervalReplies { get; set; } /// /// Repliesのチェックを実行するかどうかを指定します。 /// public Boolean EnableRepliesCheck { get; set; } /// /// タイムラインの一回の取得につき何件取得するかを指定します。 /// public Int32 FetchCount { get; set; } /// /// gzip圧縮を利用するかどうかを指定します。 /// public Boolean EnableCompression { get; set; } /// /// フォローしているユーザ一覧を取得する際、次のページが存在するか判断する閾値を指定します。 /// public Int32 FriendsPerPageThreshold { get; set; } /// /// OAuthクライアントを取得します。 /// public TwitterOAuth OAuthClient { get; private set; } /// /// 認証情報を問い合わせます。 /// /// ユーザー情報 /// /// public User VerifyCredential() { return ExecuteRequest(() => { String responseBody = GET("/account/verify_credentials.xml", false); if (NilClasses.CanDeserialize(responseBody)) { return null; } else { User user = User.Serializer.Deserialize(new StringReader(responseBody)) as User; return user; } }); } /// /// ステータスを更新します。 /// /// /// public Status UpdateStatus(String message) { return UpdateStatus(message, 0); } /// /// ステータスを更新します。 /// /// /// public Status UpdateStatus(String message, Int64 inReplyToStatusId) { String encodedMessage = TwitterService.EncodeMessage(message); return ExecuteRequest(() => { String postData = String.Format("status={0}&source={1}{2}", encodedMessage, ClientName, (inReplyToStatusId != 0 ? "&in_reply_to_status_id=" + inReplyToStatusId : "")); String responseBody = POST("/statuses/update.xml", postData); if (NilClasses.CanDeserialize(responseBody)) { return null; } else { Status status = Status.Serializer.Deserialize(new StringReader(responseBody)) as Status; return status; } }); } /// /// 指定されたユーザにダイレクトメッセージを送信します。 /// /// /// public void SendDirectMessage(String screenName, String message) { String encodedMessage = TwitterService.EncodeMessage(message); ExecuteRequest(() => { String postData = String.Format("user={0}&text={1}", GetUserId(screenName), encodedMessage); String responseBody = POST("/direct_messages/new.xml", postData); }); } /// /// friendsを取得します。 /// /// /// public User[] GetFriends() { return GetFriends(1); } /// /// friendsを取得します。 /// /// /// public User[] GetFriends(Int32 maxPage) { List users = new List(); Int64 cursor = -1; Int32 page = maxPage; return ExecuteRequest(() => { while (cursor != 0 && page > 0) { String responseBody = GET(String.Format("/statuses/friends.xml?cursor={0}&lite=true", cursor)); if (NilClasses.CanDeserialize(responseBody)) { return users.ToArray(); } else { UsersList usersList = UsersList.Serializer.Deserialize(new StringReader(responseBody)) as UsersList; if (usersList == null || usersList.Users == null || usersList.Users.User == null || usersList.Users.User.Length == 0) { return users.ToArray(); } else { users.AddRange(usersList.Users.User); } --page; cursor = usersList.NextCursor; } } return users.ToArray(); }); } /// /// userを取得します。 /// /// /// public User GetUser(String screenName) { return ExecuteRequest(() => { String responseBody = GET(String.Format("/users/show.xml?screen_name={0}&include_entities=true", screenName), false); if (NilClasses.CanDeserialize(responseBody)) { return null; } else { User user = User.Serializer.Deserialize(new StringReader(responseBody)) as User; return user; } }); } /// /// 指定したIDでユーザ情報を取得します。 /// /// /// public User GetUserById(Int32 id) { return ExecuteRequest(() => { String responseBody = GET(String.Format("/users/show.xml?id={0}&include_entities=true", id), false); if (NilClasses.CanDeserialize(responseBody)) { return null; } else { User user = User.Serializer.Deserialize(new StringReader(responseBody)) as User; return user; } }); } /// /// timeline を取得します。 /// /// 最終更新日時 /// /// public Statuses GetTimeline(DateTime since) { return GetTimeline(since, FetchCount); } /// /// timeline を取得します。 /// /// 最終更新日時 /// 取得数 /// public Statuses GetTimeline(DateTime since, Int32 count) { return ExecuteRequest(() => { String responseBody = GET(String.Format("/statuses/friends_timeline.xml?since={0}&count={1}&include_entities=true", Utility.UrlEncode(since.ToUniversalTime().ToString("r")), count)); Statuses statuses; if (NilClasses.CanDeserialize(responseBody)) { statuses = new Statuses(); statuses.Status = new Status[0]; } else { statuses = Statuses.Serializer.Deserialize(new StringReader(responseBody)) as Statuses; if (statuses == null || statuses.Status == null) { statuses = new Statuses(); statuses.Status = new Status[0]; } } return statuses; }); } /// /// timeline を取得します。 /// /// 最後に取得したID /// ステータス public Statuses GetTimeline(Int64 sinceId) { return GetTimeline(sinceId, FetchCount); } /// /// timeline を取得します。 /// /// 最後に取得したID /// 取得数 /// ステータス public Statuses GetTimeline(Int64 sinceId, Int32 count) { return ExecuteRequest(() => { String responseBody = GET(String.Format("/statuses/home_timeline.xml?since_id={0}&count={1}&include_entities=true", sinceId, count)); Statuses statuses; if (NilClasses.CanDeserialize(responseBody)) { statuses = new Statuses(); statuses.Status = new Status[0]; } else { statuses = Statuses.Serializer.Deserialize(new StringReader(responseBody)) as Statuses; if (statuses == null || statuses.Status == null) { statuses = new Statuses(); statuses.Status = new Status[0]; } } return statuses; }); } /// /// 指定したIDでステータスを取得します。 /// /// /// public Status GetStatusById(Int64 id) { return ExecuteRequest(() => { String responseBody = GET(String.Format("/statuses/show.xml?id={0}&include_entities=true", id), false); if (NilClasses.CanDeserialize(responseBody)) { return null; } else { Status status = Status.Serializer.Deserialize(new StringReader(responseBody)) as Status; return status; } }); } /// /// replies を取得します。 /// /// /// [Obsolete] public Statuses GetReplies() { return GetMentions(); } /// /// mentions を取得します。 /// /// /// public Statuses GetMentions() { return GetMentions(1); } /// /// mentions を取得します。 /// /// /// public Statuses GetMentions(Int64 sinceId) { return ExecuteRequest(() => { String responseBody = GET(String.Format("/statuses/mentions.xml?since_id={0}&include_entities=true", sinceId)); Statuses statuses; if (NilClasses.CanDeserialize(responseBody)) { statuses = new Statuses(); statuses.Status = new Status[0]; } else { statuses = Statuses.Serializer.Deserialize(new StringReader(responseBody)) as Statuses; if (statuses == null || statuses.Status == null) { statuses = new Statuses(); statuses.Status = new Status[0]; } } return statuses; }); } /// /// direct messages を取得します。 /// /// 最終更新日時 /// /// public DirectMessages GetDirectMessages(DateTime since) { return ExecuteRequest(() => { // Cookie ではダメ String responseBody = GET(String.Format("/direct_messages.xml?since={0}", Utility.UrlEncode(since.ToUniversalTime().ToString("r"))), false); DirectMessages directMessages; if (NilClasses.CanDeserialize(responseBody)) { // 空 directMessages = new DirectMessages(); directMessages.DirectMessage = new DirectMessage[0]; } else { directMessages = DirectMessages.Serializer.Deserialize(new StringReader(responseBody)) as DirectMessages; if (directMessages == null || directMessages.DirectMessage == null) { directMessages = new DirectMessages(); directMessages.DirectMessage = new DirectMessage[0]; } } return directMessages; }); } /// /// direct messages を取得します。 /// /// 最後に取得したID /// /// public DirectMessages GetDirectMessages(Int64 sinceId) { return ExecuteRequest(() => { // Cookie ではダメ String responseBody = GET(String.Format("/direct_messages.xml?since_id={0}", sinceId), false); DirectMessages directMessages; if (NilClasses.CanDeserialize(responseBody)) { // 空 directMessages = new DirectMessages(); directMessages.DirectMessage = new DirectMessage[0]; } else { directMessages = DirectMessages.Serializer.Deserialize(new StringReader(responseBody)) as DirectMessages; if (directMessages == null || directMessages.DirectMessage == null) { directMessages = new DirectMessages(); directMessages.DirectMessage = new DirectMessage[0]; } } return directMessages; }); } /// スクリーンネーム /// 最終更新日時 /// /// /// public Statuses GetTimelineByScreenName(String screenName, DateTime since, Int32 count) { return ExecuteRequest(() => { StringBuilder sb = new StringBuilder(); if (since != new DateTime()) sb.Append("since=").Append(Utility.UrlEncode(since.ToUniversalTime().ToString("r"))).Append("&"); if (count > 0) sb.Append("count=").Append(count).Append("&"); String responseBody = GET(String.Format("/statuses/user_timeline.xml?screen_name={0}&{1}", screenName, sb.ToString())); Statuses statuses; if (NilClasses.CanDeserialize(responseBody)) { statuses = new Statuses(); statuses.Status = new Status[0]; } else { statuses = Statuses.Serializer.Deserialize(new StringReader(responseBody)) as Statuses; if (statuses == null || statuses.Status == null) { statuses = new Statuses(); statuses.Status = new Status[0]; } } return statuses; }); } /// /// 指定したユーザの favorites を取得します。 /// /// スクリーンネーム /// ページ /// /// public Statuses GetFavoritesByScreenName(String screenName, Int32 page) { return ExecuteRequest(() => { StringBuilder sb = new StringBuilder(); if (page > 0) sb.Append("page=").Append(page).Append("&"); String responseBody = GET(String.Format("/favorites.xml?screen_name={0}&{1}", screenName, sb.ToString())); Statuses statuses; if (NilClasses.CanDeserialize(responseBody)) { statuses = new Statuses(); statuses.Status = new Status[0]; } else { statuses = Statuses.Serializer.Deserialize(new StringReader(responseBody)) as Statuses; if (statuses == null || statuses.Status == null) { statuses = new Statuses(); statuses.Status = new Status[0]; } } return statuses; }); } /// /// メッセージをfavoritesに追加します。 /// /// /// public Status CreateFavorite(Int64 id) { return ExecuteRequest(() => { String responseBody = POST(String.Format("/favorites/create/{0}.xml", id), ""); Status status; if (NilClasses.CanDeserialize(responseBody)) { return null; } else { status = Status.Serializer.Deserialize(new StringReader(responseBody)) as Status; return status; } }); } /// /// メッセージをfavoritesから削除します。 /// /// /// public Status DestroyFavorite(Int64 id) { return ExecuteRequest(() => { String responseBody = POST(String.Format("/favorites/destroy/{0}.xml", id), ""); Status status; if (NilClasses.CanDeserialize(responseBody)) { return null; } else { status = Status.Serializer.Deserialize(new StringReader(responseBody)) as Status; return status; } }); } /// /// メッセージを削除します。 /// /// /// public Status DestroyStatus(Int64 id) { return ExecuteRequest(() => { String responseBody = POST(String.Format("/statuses/destroy/{0}.xml", id), ""); Status status; if (NilClasses.CanDeserialize(responseBody)) { return null; } else { status = Status.Serializer.Deserialize(new StringReader(responseBody)) as Status; return status; } }); } /// /// メッセージをretweetします。 /// /// /// public Status RetweetStatus(Int64 id) { return ExecuteRequest(() => { String responseBody = POST(String.Format("/statuses/retweet/{0}.xml?include_entities=true", id), ""); if (NilClasses.CanDeserialize(responseBody)) { return null; } else { Status status = Status.Serializer.Deserialize(new StringReader(responseBody)) as Status; return status; } }); } /// /// ユーザをfollowします。 /// /// /// public User CreateFriendship(String screenName) { return ExecuteRequest(() => { String postData = String.Format("screen_name={0}", screenName); String responseBody = POST("/friendships/create.xml", postData); if (NilClasses.CanDeserialize(responseBody)) { return null; } else { return User.Serializer.Deserialize(new StringReader(responseBody)) as User; } }); } /// /// ユーザをremoveします。 /// /// /// public User DestroyFriendship(String screenName) { return ExecuteRequest(() => { String postData = String.Format("screen_name={0}", screenName); String responseBody = POST("/friendships/destroy.xml", postData); if (NilClasses.CanDeserialize(responseBody)) { return null; } else { return User.Serializer.Deserialize(new StringReader(responseBody)) as User; } }); } /// /// ユーザをblockします。 /// /// /// public User CreateBlock(String screenName) { return ExecuteRequest(() => { String postData = String.Format("screen_name={0}", screenName); String responseBody = POST("/blocks/create.xml", postData); if (NilClasses.CanDeserialize(responseBody)) { return null; } else { return User.Serializer.Deserialize(new StringReader(responseBody)) as User; } }); } /// /// ユーザへのblockを解除します。 /// /// /// public User DestroyBlock(String screenName) { return ExecuteRequest(() => { String postData = String.Format("screen_name={0}", screenName); String responseBody = POST("/blocks/destroy.xml", postData); if (NilClasses.CanDeserialize(responseBody)) { return null; } else { return User.Serializer.Deserialize(new StringReader(responseBody)) as User; } }); } #region 内部タイマーイベント /// /// Twitter のタイムラインの受信を開始します。 /// public void Start() { // HACK: dueTime を指定しないとMonoで動かないことがある _timer.Change(0, Interval * 1000); _timerDirectMessage.Change(1000, IntervalDirectMessage * 1000); if (EnableRepliesCheck) { _timerReplies.Change(2000, IntervalReplies * 1000); } } /// /// Twitter のタイムラインの受信を停止します。 /// public void Stop() { _timer.Change(Timeout.Infinite, Timeout.Infinite); _timerDirectMessage.Change(Timeout.Infinite, Timeout.Infinite); _timerReplies.Change(Timeout.Infinite, Timeout.Infinite); } /// /// /// /// private void OnTimerCallback(Object stateObject) { RunCallback(_timer, CheckNewTimeLine); } /// /// /// /// private void OnTimerCallbackDirectMessage(Object stateObject) { RunCallback(_timerDirectMessage, CheckDirectMessage); } /// /// /// /// private void OnTimerCallbackReplies(Object stateObject) { RunCallback(_timerReplies, CheckNewReplies); } /// /// 既に受信したstatusかどうかをチェックします。既に送信済みの場合falseを返します。 /// /// /// /// private Boolean ProcessDropProtection(LinkedList statusBuffer, Int64 statusId) { // 差分チェック lock (statusBuffer) { if (statusBuffer.Contains(statusId)) return false; statusBuffer.AddLast(statusId); if (statusBuffer.Count > BufferSize) { // 一番古いのを消す statusBuffer.RemoveFirst(); } } return true; } /// /// 最終更新IDを更新します。 /// /// private void UpdateLastAccessStatusId(Status status, ref Int64 sinceId) { if (ProcessDropProtection(_statusBuffer, status.Id)) { if (EnableDropProtection) { // 取りこぼし防止しているときは一番古いID if (status.Id < sinceId) { sinceId = status.Id; } } else { if (status.Id > sinceId) { sinceId = status.Id; } } } } /// /// ステータスがすでに流されたかどうかをチェックして、流されていない場合に指定されたアクションを実行します。 /// /// /// public void ProcessStatus(Status status, Action action) { if (ProcessDropProtection(_statusBuffer, status.Id)) { action(status); UpdateLastAccessStatusId(status, ref _lastAccessTimelineId); } } /// /// ステータスがすでに流されたかどうかをチェックして、流されていない場合に指定されたアクションを実行します。 /// /// /// public void ProcessStatuses(Statuses statuses, Action action) { Statuses tmpStatuses = new Statuses(); List statusList = new List(); foreach (Status status in statuses.Status) { ProcessStatus(status, s => { statusList.Add(status); UpdateLastAccessStatusId(status, ref _lastAccessTimelineId); }); } if (statusList.Count == 0) return; tmpStatuses.Status = statusList.ToArray(); action(tmpStatuses); } /// /// Repliesステータスがすでに流されたかどうかをチェックして、流されていない場合に指定されたアクションを実行します。 /// /// /// public void ProcessRepliesStatus(Statuses statuses, Action action) { Statuses tmpStatuses = new Statuses(); List statusList = new List(); foreach (Status status in statuses.Status.Where(s => s.Id > _lastAccessRepliesId)) { if (ProcessDropProtection(_repliesBuffer, status.Id) && ProcessDropProtection(_statusBuffer, status.Id)) { statusList.Add(status); } UpdateLastAccessStatusId(status, ref _lastAccessTimelineId); UpdateLastAccessStatusId(status, ref _lastAccessRepliesId); } if (statusList.Count == 0) return; tmpStatuses.Status = statusList.ToArray(); action(tmpStatuses); } private void CheckNewTimeLine() { Boolean friendsCheckRequired = false; RunCheck(delegate { Statuses statuses = GetTimeline(_lastAccessTimelineId); Array.Reverse(statuses.Status); // 差分チェック ProcessStatuses(statuses, (s) => { OnTimelineStatusesReceived(new StatusesUpdatedEventArgs(s, _isFirstTime, friendsCheckRequired)); }); if (_isFirstTime || !EnableDropProtection) { if (statuses.Status != null && statuses.Status.Length > 0) _lastAccessTimelineId = statuses.Status.Select(s => s.Id).Max(); } _isFirstTime = false; }); } private void CheckDirectMessage() { RunCheck(delegate { DirectMessages directMessages = (_lastAccessDirectMessageId == 0) ? GetDirectMessages(_lastAccessDirectMessage) : GetDirectMessages(_lastAccessDirectMessageId); Array.Reverse(directMessages.DirectMessage); foreach (DirectMessage message in directMessages.DirectMessage) { // チェック if (message == null || String.IsNullOrEmpty(message.SenderScreenName)) { continue; } OnDirectMessageReceived(new DirectMessageEventArgs(message, _isFirstTimeDirectMessage)); // 最終更新時刻 if (message.Id > _lastAccessDirectMessageId) { _lastAccessDirectMessage = message.CreatedAt; _lastAccessDirectMessageId = message.Id; } } _isFirstTimeDirectMessage = false; }); } private void CheckNewReplies() { Boolean friendsCheckRequired = false; RunCheck(delegate { Statuses statuses = GetMentions(_lastAccessRepliesId); Array.Reverse(statuses.Status); // 差分チェック ProcessRepliesStatus(statuses, (s) => { // Here I pass dummy, because no matter how the replier flags // friendsCheckRequired, we cannot receive his or her info // through get_friends. OnRepliesReceived(new StatusesUpdatedEventArgs(s, _isFirstTimeReplies, friendsCheckRequired)); }); if (_isFirstTimeReplies || !EnableDropProtection) { if (statuses.Status != null && statuses.Status.Length > 0) _lastAccessRepliesId = statuses.Status.Select(s => s.Id).Max(); } _isFirstTimeReplies = false; }); } #endregion #region イベント protected virtual void OnCheckError(ErrorEventArgs e) { FireEvent(CheckError, e); } protected virtual void OnTimelineStatusesReceived(StatusesUpdatedEventArgs e) { FireEvent(TimelineStatusesReceived, e); } protected virtual void OnRepliesReceived(StatusesUpdatedEventArgs e) { FireEvent(RepliesReceived, e); } protected virtual void OnDirectMessageReceived(DirectMessageEventArgs e) { FireEvent(DirectMessageReceived, e); } private void FireEvent(EventHandler eventHandler, T e) where T : EventArgs { if (eventHandler != null) eventHandler(this, e); } #endregion #region ユーティリティメソッド /// /// /// /// /// private static String EncodeMessage(String s) { return Utility.UrlEncode(s); } /// /// 必要に応じてIDに変換する /// /// /// private String GetUserId(String screenName) { Int32 id; if (Int32.TryParse(screenName, out id)) { return GetUser(screenName).Id.ToString(); } else { return screenName; } } #endregion #region Helper Delegate private delegate T ExecuteRequestProcedure(); private delegate void Procedure(); private T ExecuteRequest(ExecuteRequestProcedure execProc) { try { return execProc(); } catch (WebException) { throw; } catch (InvalidOperationException ioe) { // XmlSerializer throw new TwitterServiceException(ioe); } catch (XmlException xe) { throw new TwitterServiceException(xe); } catch (IOException ie) { throw new TwitterServiceException(ie); } } private void ExecuteRequest(Procedure execProc) { try { execProc(); } catch (WebException) { throw; } catch (InvalidOperationException ioe) { // XmlSerializer throw new TwitterServiceException(ioe); } catch (XmlException xe) { throw new TwitterServiceException(xe); } catch (IOException ie) { throw new TwitterServiceException(ie); } } /// /// チェックを実行します。例外が発生した場合には自動的にメッセージを送信します。 /// /// 実行するチェック処理 /// private Boolean RunCheck(Procedure proc) { try { proc(); } catch (WebException ex) { HttpWebResponse webResponse = ex.Response as HttpWebResponse; if (ex.Response == null || webResponse != null || webResponse.StatusCode != HttpStatusCode.NotModified) { // not-modified 以外 OnCheckError(new ErrorEventArgs(ex)); return false; } } catch (TwitterServiceException ex2) { try { OnCheckError(new ErrorEventArgs(ex2)); } catch { } return false; } catch (Exception ex3) { try { OnCheckError(new ErrorEventArgs(ex3)); } catch { } TraceLogger.Twitter.Information("RunCheck(Unhandled Exception): " + ex3.ToString()); return false; } return true; } /// /// タイマーコールバックの処理を実行します。 /// /// /// private void RunCallback(Timer timer, Procedure callbackProcedure) { // あまりに処理が遅れると二重になる可能性がある if (timer != null && Monitor.TryEnter(timer)) { try { callbackProcedure(); } finally { Monitor.Exit(timer); } } } #endregion #region IDisposable メンバ public void Dispose() { //if (_webClient != null) //{ // _webClient.Dispose(); // _webClient = null; //} Stop(); if (_timer != null) { _timer.Dispose(); _timer = null; } if (_timerDirectMessage != null) { _timerDirectMessage.Dispose(); _timerDirectMessage = null; } if (_timerReplies != null) { _timerReplies.Dispose(); _timerReplies = null; } GC.SuppressFinalize(this); _Counter.Decrement(ref _Counter.TwitterService); } #endregion internal class PreAuthenticatedWebClient : WebClient { private TwitterService _twitterService; public PreAuthenticatedWebClient(TwitterService twitterService) { _twitterService = twitterService; } protected override WebRequest GetWebRequest(Uri address) { // このアプリケーションで HttpWebReqeust 以外がくることはない HttpWebRequest webRequest = base.GetWebRequest(address) as HttpWebRequest; webRequest.PreAuthenticate = true; webRequest.Accept = "text/xml, application/xml"; webRequest.UserAgent = String.Format("{0}/{1}", _twitterService.ClientName, GetType().Assembly.GetName().Version); //webRequest.Referer = TwitterService.Referer; webRequest.Headers["X-Twitter-Client"] = _twitterService.ClientName; webRequest.Headers["X-Twitter-Client-Version"] = _twitterService.ClientVersion; webRequest.Headers["X-Twitter-Client-URL"] = _twitterService.ClientUrl; if (_twitterService.EnableCompression) webRequest.Headers["Accept-Encoding"] = "gzip"; return webRequest; } } /// /// 指定されたURLからデータを取得し文字列として返します。CookieLoginModeが有効なときは自動的にCookieログイン状態で取得します。 /// /// データを取得するURL /// public String GET(String url) { return GET(url, POSTFetchMode); } /// /// 指定されたURLからデータを取得し文字列として返します。 /// /// データを取得するURL /// POSTで取得するかどうか /// public String GET(String url, Boolean postFetchMode) { TraceLogger.Twitter.Information("GET: " + url); if (OAuthClient == null) { return GETWithBasicAuth(url, postFetchMode); } else { return GETWithOAuth(url); } } public String POST(String url, String postData) { TraceLogger.Twitter.Information("POST: " + url); if (OAuthClient == null) { return POSTWithBasicAuth(url, Encoding.UTF8.GetBytes(postData)); } else { return OAuthClient.Request(new Uri(ServiceServerPrefix + url), TwitterOAuth.HttpMethod.POST, postData); } } #region Basic 認証アクセス private String GETWithBasicAuth(String url, Boolean postFetchMode) { if (postFetchMode) { return POST(url, ""); } else { url = ServiceServerPrefix + url; HttpWebRequest webRequest = CreateHttpWebRequest(url, "GET"); HttpWebResponse webResponse = webRequest.GetResponse() as HttpWebResponse; using (StreamReader sr = new StreamReader(GetResponseStream(webResponse))) return sr.ReadToEnd(); } } private String POSTWithBasicAuth(String url, Byte[] postData) { url = ServiceServerPrefix + url; HttpWebRequest webRequest = CreateHttpWebRequest(url, "POST"); using (Stream stream = webRequest.GetRequestStream()) { stream.Write(postData, 0, postData.Length); } HttpWebResponse webResponse = webRequest.GetResponse() as HttpWebResponse; using (StreamReader sr = new StreamReader(GetResponseStream(webResponse))) return sr.ReadToEnd(); } //[Obsolete] protected virtual HttpWebRequest CreateHttpWebRequest(String url, String method) { HttpWebRequest webRequest = HttpWebRequest.Create(url) as HttpWebRequest; //webRequest.Credentials = _credential; //webRequest.PreAuthenticate = true; webRequest.ServicePoint.Expect100Continue = false; webRequest.Proxy = _proxy; webRequest.Method = method; webRequest.Accept = "text/xml, application/xml, text/html;q=0.5"; webRequest.UserAgent = String.Format("{0}/{1}", ClientName, ClientVersion); //webRequest.Referer = TwitterService.Referer; webRequest.Headers["X-Twitter-Client"] = ClientName; webRequest.Headers["X-Twitter-Client-Version"] = ClientVersion; webRequest.Headers["X-Twitter-Client-URL"] = ClientUrl; if (EnableCompression) webRequest.Headers["Accept-Encoding"] = "gzip"; Uri uri = new Uri(url); NetworkCredential cred = _credential.GetCredential(uri, "Basic"); webRequest.Headers["Authorization"] = String.Format("Basic {0}", Convert.ToBase64String(Encoding.UTF8.GetBytes(String.Format("{0}:{1}", cred.UserName, cred.Password)))); return webRequest as HttpWebRequest; } #endregion #region OAuth 認証アクセス private String GETWithOAuth(String url) { HttpWebRequest webRequest = OAuthClient.CreateRequest(new Uri(ServiceServerPrefix + url), TwitterOAuth.HttpMethod.GET); if (EnableCompression) webRequest.Headers["Accept-Encoding"] = "gzip"; HttpWebResponse webResponse = webRequest.GetResponse() as HttpWebResponse; using (StreamReader sr = new StreamReader(GetResponseStream(webResponse))) return sr.ReadToEnd(); } #endregion private Stream GetResponseStream(WebResponse webResponse) { HttpWebResponse httpWebResponse = webResponse as HttpWebResponse; if (httpWebResponse == null) return webResponse.GetResponseStream(); if (String.Compare(httpWebResponse.ContentEncoding, "gzip", true) == 0) return new GZipStream(webResponse.GetResponseStream(), CompressionMode.Decompress); return webResponse.GetResponseStream(); } #region Cookie アクセス private CookieCollection _cookies = null; [Obsolete("Cookieによる認証はサポートされません。代わりにGET(POST)を利用してください。")] public String GETWithCookie(String url) { Boolean isRetry = false; url = ServiceServerPrefix + url; Retry: try { TraceLogger.Twitter.Information("GET(Cookie): {0}", url); return DownloadString(url); } catch (WebException we) { HttpWebResponse wResponse = we.Response as HttpWebResponse; if (wResponse == null || wResponse.StatusCode != HttpStatusCode.Unauthorized || isRetry) throw; _cookies = CookieLogin(); isRetry = true; goto Retry; } } [Obsolete("Cookieによる認証はサポートされません。")] public CookieCollection CookieLogin() { TraceLogger.Twitter.Information("Cookie Login: {0}", _userName); HttpWebRequest request = CreateWebRequest("http://twitter.com/account/verify_credentials.xml") as HttpWebRequest; request.AllowAutoRedirect = false; request.Method = "GET"; NetworkCredential cred = _credential.GetCredential(new Uri("http://twitter.com/account/verify_credentials.xml"), "Basic"); request.Headers["Authorization"] = String.Format("Basic {0}", Convert.ToBase64String(Encoding.UTF8.GetBytes(String.Format("{0}:{1}", cred.UserName, cred.Password)))); HttpWebResponse response = request.GetResponse() as HttpWebResponse; using (StreamReader sr = new StreamReader(GetResponseStream(response), Encoding.UTF8)) { String responseBody = sr.ReadToEnd(); if (response.Cookies.Count == 0) { throw new ApplicationException("ログインに失敗しました。ユーザ名またはパスワードが間違っている可能性があります。"); } foreach (Cookie cookie in response.Cookies) { cookie.Domain = "twitter.com"; } _cookies = response.Cookies; return response.Cookies; } } [Obsolete("Cookieによる認証はサポートされません。")] WebRequest CreateWebRequest(String uri) { WebRequest request = WebRequest.Create(uri); if (request is HttpWebRequest) { HttpWebRequest httpRequest = request as HttpWebRequest; httpRequest.UserAgent = "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1)"; httpRequest.Referer = Referer; httpRequest.PreAuthenticate = false; httpRequest.Accept = "*/*"; httpRequest.CookieContainer = new CookieContainer(); httpRequest.Proxy = _proxy; if (EnableCompression) httpRequest.Headers["Accept-Encoding"] = "gzip"; if (_cookies != null) { httpRequest.CookieContainer.Add(_cookies); } } return request; } String DownloadString(String url) { WebRequest request = CreateWebRequest(url); WebResponse response = null; try { response = request.GetResponse(); using (StreamReader sr = new StreamReader(GetResponseStream(response))) { return sr.ReadToEnd(); } } finally { if (response != null) { response.Close(); } } } #endregion } /// /// エラー発生時のイベントのデータを提供します。 /// public class ErrorEventArgs : EventArgs { public Exception Exception { get; set; } public ErrorEventArgs(Exception ex) { this.Exception = ex; } } /// /// ステータスが更新時のイベントのデータを提供します。 /// public class StatusesUpdatedEventArgs : EventArgs { public Statuses Statuses { get; set; } public Boolean IsFirstTime { get; set; } public Boolean FriendsCheckRequired { get; set; } public StatusesUpdatedEventArgs(Statuses statuses) { this.Statuses = statuses; } public StatusesUpdatedEventArgs(Statuses statuses, Boolean isFirstTime, Boolean friendsCheckRequired) : this(statuses) { this.IsFirstTime = isFirstTime; this.FriendsCheckRequired = friendsCheckRequired; } } /// /// ダイレクトメッセージを受信時のイベントのデータを提供します。 /// public class DirectMessageEventArgs : EventArgs { public DirectMessage DirectMessage { get; set; } public Boolean IsFirstTime { get; set; } public DirectMessageEventArgs(DirectMessage directMessage) { this.DirectMessage = directMessage; } public DirectMessageEventArgs(DirectMessage directMessage, Boolean isFirstTime) : this(directMessage) { this.IsFirstTime = isFirstTime; } } /// /// Twitterにアクセスを試みた際にスローされる例外。 /// public class TwitterServiceException : ApplicationException { public TwitterServiceException(String message) : base(message) { } public TwitterServiceException(Exception innerException) : base(innerException.Message, innerException) { } public TwitterServiceException(String message, Exception innerException) : base(message, innerException) { } } /// /// データが空を表します。 /// [XmlRoot("nilclasses")] public class NilClasses { private static Object _syncObject = new object(); private static XmlSerializer _serializer = null; static NilClasses() { lock (_syncObject) { if (_serializer == null) { _serializer = new XmlSerializer(typeof(NilClasses)); } } } public static XmlSerializer Serializer { get { return _serializer; } } public static Boolean CanDeserialize(String xml) { return NilClasses.Serializer.CanDeserialize(new XmlTextReader(new StringReader(xml))); } } /// /// DirectMessage のセットを格納します。 /// [XmlRoot("direct-messages")] public class DirectMessages { [XmlElement("direct_message")] public DirectMessage[] DirectMessage; private static Object _syncObject = new object(); private static XmlSerializer _serializer = null; static DirectMessages() { lock (_syncObject) { if (_serializer == null) { _serializer = new XmlSerializer(typeof(DirectMessages)); } } } public static XmlSerializer Serializer { get { return _serializer; } } public DirectMessages() { _Counter.Increment(ref _Counter.DirectMessages); } ~DirectMessages() { _Counter.Decrement(ref _Counter.DirectMessages); } } /// /// ダイレクトメッセージの情報を表します。 /// [XmlType("DirectMessage")] public class DirectMessage { [XmlElement("id")] public Int64 Id; [XmlElement("text")] public String _text; [XmlElement("sender_id")] public String SenderId; [XmlElement("recipient_id")] public String RecipientId; [XmlElement("created_at")] public String _createdAt; [XmlElement("sender_screen_name")] public String SenderScreenName; [XmlElement("recipient_screen_name")] public String RecipientScreenName; [XmlIgnore] public String Text { get { if (String.IsNullOrEmpty(_text)) return String.Empty; return Utility.UnescapeCharReference(_text); } } [XmlIgnore] public DateTime CreatedAt { get { return Utility.ParseDateTime(_createdAt); } } public DirectMessage() { _Counter.Increment(ref _Counter.DirectMessage); } ~DirectMessage() { _Counter.Decrement(ref _Counter.DirectMessage); } public override string ToString() { return String.Format("DirectMessage: {0} (ID:{1})", Text, Id.ToString()); } } /// /// Statusのセットを格納します。 /// [XmlType("statuses")] public class Statuses { [XmlElement("status")] public Status[] Status; private static Object _syncObject = new object(); private static XmlSerializer _serializer = null; static Statuses() { lock (_syncObject) { if (_serializer == null) { _serializer = new XmlSerializer(typeof(Statuses)); } } } public static XmlSerializer Serializer { get { return _serializer; } } public Statuses() { _Counter.Increment(ref _Counter.Statuses); } ~Statuses() { _Counter.Decrement(ref _Counter.Statuses); } } /// /// Userのセットを格納します。 /// [XmlType("users_list")] public class UsersList { [XmlElement("users")] public Users Users { get; set; } [XmlElement("next_cursor")] public Int64 NextCursor { get; set; } [XmlElement("previous_cursor")] public Int64 PreviousCursor { get; set; } private static Object _syncObject = new object(); private static XmlSerializer _serializer = null; static UsersList() { lock (_syncObject) { if (_serializer == null) { _serializer = new XmlSerializer(typeof(UsersList)); } } } public static XmlSerializer Serializer { get { return _serializer; } } } /// /// Userのセットを格納します。 /// [XmlType("users")] public class Users { [XmlElement("user")] public User[] User; private static Object _syncObject = new object(); private static XmlSerializer _serializer = null; static Users() { lock (_syncObject) { if (_serializer == null) { _serializer = new XmlSerializer(typeof(Users)); } } } public static XmlSerializer Serializer { get { return _serializer; } } public Users() { _Counter.Increment(ref _Counter.Users); } ~Users() { _Counter.Decrement(ref _Counter.Users); } } [XmlType("user")] public class User { [XmlElement("id")] public Int32 Id; [XmlElement("name")] public String Name; [XmlElement("screen_name")] public String ScreenName; [XmlElement("location")] public String Location; [XmlElement("description")] public String Description; [XmlElement("profile_image_url")] public String ProfileImageUrl; [XmlElement("url")] public String Url; [XmlElement("protected")] public Boolean Protected; [XmlElement("status")] public Status Status; //[XmlElement("following")] [XmlIgnore] public Boolean Following; private static Object _syncObject = new object(); private static XmlSerializer _serializer = null; static User() { lock (_syncObject) { if (_serializer == null) { _serializer = new XmlSerializer(typeof(User)); } } } public static XmlSerializer Serializer { get { return _serializer; } } public User() { _Counter.Increment(ref _Counter.User); } ~User() { _Counter.Decrement(ref _Counter.User); } public override string ToString() { return String.Format("User: {0} / {1} (ID:{2})", ScreenName, Name, Id.ToString()); } } /// /// ステータスを表します。 /// [XmlType("status")] public class Status { [XmlElement("created_at")] public String _createdAtOriginal; [XmlElement("id")] public Int64 Id; [XmlElement("in_reply_to_status_id")] public String InReplyToStatusId; [XmlElement("in_reply_to_user_id")] public String InReplyToUserId; [XmlElement("retweeted_status")] public Status RetweetedStatus; [XmlElement("text")] public String _textOriginal; [XmlElement("user")] public User User; [XmlElement("source")] public String Source; [XmlElement("favorited")] public String Favorited; [XmlElement("truncated")] public Boolean Truncated; [XmlElement("retweet_count")] public String RetweetCount; [XmlElement("retweeted")] public Boolean Retweeted; [XmlElement("entities")] public Entities Entities; [XmlIgnore] private String _text; [XmlIgnore] private DateTime _createdAt; [XmlIgnore] public String Text { get { if (!String.IsNullOrEmpty(_textOriginal) && _text == null) { _text = Utility.UnescapeCharReference(_textOriginal); } return _text ?? ""; } set { _text = value; } } [XmlIgnore] public DateTime CreatedAt { get { if (!String.IsNullOrEmpty(_createdAtOriginal) && _createdAt == DateTime.MinValue) { _createdAt = Utility.ParseDateTime(_createdAtOriginal); } return _createdAt; } set { _createdAt = value; } } private static Object _syncObject = new object(); private static XmlSerializer _serializer = null; static Status() { lock (_syncObject) { if (_serializer == null) { _serializer = new XmlSerializer(typeof(Status)); } } } public Status() { _Counter.Increment(ref _Counter.Status); } ~Status() { _Counter.Decrement(ref _Counter.Status); } public static XmlSerializer Serializer { get { return _serializer; } } public override int GetHashCode() { return (Int32)(this.Id - Int32.MaxValue); } public override bool Equals(object obj) { if (!(obj is Status)) return false; Status status = obj as Status; return (status.Id == this.Id) && (status.Text == this.Text); } public override string ToString() { return String.Format("Status: {0} (ID:{1})", Text, Id.ToString()); } } /// /// エンティティの情報を表します。 /// [XmlType("entities")] public class Entities { //[XmlElement("media")] //public MediaEntity[] Media; [XmlArray("urls")] public UrlEntity[] Urls; [XmlArray("hashtags")] public HashtagEntity[] Hashtags; } /// /// URLエンティティの情報を表します。 /// [XmlType("url")] public class UrlEntity { [XmlAttribute("start")] public Int32 Start; [XmlAttribute("end")] public Int32 End; [XmlElement("url")] public String Url; [XmlElement("display_url")] public String DisplayUrl; [XmlElement("expanded_url")] public String ExpandedUrl; } /// /// URLエンティティの情報を表します。 /// [XmlType("hashtag")] public class HashtagEntity { [XmlAttribute("start")] public Int32 Start; [XmlAttribute("end")] public Int32 End; [XmlElement("text")] public String Text; } ///// ///// メディアエンティティの情報を表します。 ///// //[XmlType("creative")] //public class MediaEntity //{ // [XmlElement("id")] // public Int64 Id; // [XmlElement("id_str")] // public String IdStr; // [XmlElement("media_url")] // public String MediaUrl; // [XmlElement("media_url_https")] // public String MediaUrlHttps; // [XmlElement("url")] // public String Url; // [XmlElement("display_url")] // public String DisplayUrl; // [XmlElement("expanded_url")] // public String ExpandedUrl; // [XmlElement("sizes")] // public MediaEntity.EntitySizes Sizes; // [XmlElement("type")] // public String Type; // [XmlAttribute("start")] // public Int32 Start; // [XmlAttribute("end")] // public Int32 End; // [XmlType("size")] // public class EntitySize // { // [XmlElement("resize")] // public String Resize; // [XmlElement("w")] // public Int32 W; // [XmlElement("h")] // public Int32 H; // } // [XmlType("size")] // public class EntitySizes // { // [XmlElement("large")] // public EntitySize Large; // [XmlElement("medium")] // public EntitySize Medium; // [XmlElement("small")] // public EntitySize Small; // [XmlElement("thumb")] // public EntitySize Thumb; // } //} }