1 // OpenTween - Client of Twitter
2 // Copyright (c) 2007-2011 kiri_feather (@kiri_feather) <kiri.feather@gmail.com>
3 // (c) 2008-2011 Moz (@syo68k)
4 // (c) 2008-2011 takeshik (@takeshik) <http://www.takeshik.org/>
5 // (c) 2010-2011 anis774 (@anis774) <http://d.hatena.ne.jp/anis774/>
6 // (c) 2010-2011 fantasticswallow (@f_swallow) <http://twitter.com/f_swallow>
7 // (c) 2011 Egtra (@egtra) <http://dev.activebasic.com/egtra/>
8 // (c) 2013 kim_upsilon (@kim_upsilon) <https://upsilo.net/~upsilon/>
9 // All rights reserved.
11 // This file is part of OpenTween.
13 // This program is free software; you can redistribute it and/or modify it
14 // under the terms of the GNU General Public License as published by the Free
15 // Software Foundation; either version 3 of the License, or (at your option)
18 // This program is distributed in the hope that it will be useful, but
19 // WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
20 // or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
23 // You should have received a copy of the GNU General Public License along
24 // with this program. If not, see <http://www.gnu.org/licenses/>, or write to
25 // the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor,
26 // Boston, MA 02110-1301, USA.
28 using System.Diagnostics;
32 using System.Runtime.CompilerServices;
33 using System.Runtime.Serialization;
34 using System.Runtime.Serialization.Json;
36 using System.Text.RegularExpressions;
37 using System.Threading;
38 using System.Threading.Tasks;
41 using System.Xml.Linq;
42 using System.Xml.XPath;
44 using System.Reflection;
45 using System.Collections.Generic;
47 using System.Windows.Forms;
49 using OpenTween.Connection;
53 public class Twitter : IDisposable
55 #region Regexp from twitter-text-js
57 // The code in this region code block incorporates works covered by
58 // the following copyright and permission notices:
60 // Copyright 2011 Twitter, Inc.
62 // Licensed under the Apache License, Version 2.0 (the "License"); you
63 // may not use this work except in compliance with the License. You
64 // may obtain a copy of the License in the LICENSE file, or at:
66 // http://www.apache.org/licenses/LICENSE-2.0
68 // Unless required by applicable law or agreed to in writing, software
69 // distributed under the License is distributed on an "AS IS" BASIS,
70 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
71 // implied. See the License for the specific language governing
72 // permissions and limitations under the License.
75 private const string LATIN_ACCENTS = @"\u00c0-\u00d6\u00d8-\u00f6\u00f8-\u00ff\u0100-\u024f\u0253\u0254\u0256\u0257\u0259\u025b\u0263\u0268\u026f\u0272\u0289\u028b\u02bb\u1e00-\u1eff";
76 private const string NON_LATIN_HASHTAG_CHARS = @"\u0400-\u04ff\u0500-\u0527\u1100-\u11ff\u3130-\u3185\uA960-\uA97F\uAC00-\uD7AF\uD7B0-\uD7FF";
77 //private const string CJ_HASHTAG_CHARACTERS = @"\u30A1-\u30FA\uFF66-\uFF9F\uFF10-\uFF19\uFF21-\uFF3A\uFF41-\uFF5A\u3041-\u3096\u3400-\u4DBF\u4E00-\u9FFF\u20000-\u2A6DF\u2A700-\u2B73F\u2B740-\u2B81F\u2F800-\u2FA1F";
78 private const string CJ_HASHTAG_CHARACTERS = @"\u30A1-\u30FA\u30FC\u3005\uFF66-\uFF9F\uFF10-\uFF19\uFF21-\uFF3A\uFF41-\uFF5A\u3041-\u309A\u3400-\u4DBF\p{IsCJKUnifiedIdeographs}";
79 private const string HASHTAG_BOUNDARY = @"^|$|\s|「|」|。|\.|!";
80 private const string HASHTAG_ALPHA = "[a-z_" + LATIN_ACCENTS + NON_LATIN_HASHTAG_CHARS + CJ_HASHTAG_CHARACTERS + "]";
81 private const string HASHTAG_ALPHANUMERIC = "[a-z0-9_" + LATIN_ACCENTS + NON_LATIN_HASHTAG_CHARS + CJ_HASHTAG_CHARACTERS + "]";
82 private const string HASHTAG_TERMINATOR = "[^a-z0-9_" + LATIN_ACCENTS + NON_LATIN_HASHTAG_CHARS + CJ_HASHTAG_CHARACTERS + "]";
83 public const string HASHTAG = "(" + HASHTAG_BOUNDARY + ")(#|#)(" + HASHTAG_ALPHANUMERIC + "*" + HASHTAG_ALPHA + HASHTAG_ALPHANUMERIC + "*)(?=" + HASHTAG_TERMINATOR + "|" + HASHTAG_BOUNDARY + ")";
85 private const string url_valid_preceding_chars = @"(?:[^A-Za-z0-9@@$##\ufffe\ufeff\uffff\u202a-\u202e]|^)";
86 public const string url_invalid_without_protocol_preceding_chars = @"[-_./]$";
87 private const string url_invalid_domain_chars = @"\!'#%&'\(\)*\+,\\\-\.\/:;<=>\?@\[\]\^_{|}~\$\u2000-\u200a\u0009-\u000d\u0020\u0085\u00a0\u1680\u180e\u2028\u2029\u202f\u205f\u3000\ufffe\ufeff\uffff\u202a-\u202e";
88 private const string url_valid_domain_chars = @"[^" + url_invalid_domain_chars + "]";
89 private const string url_valid_subdomain = @"(?:(?:" + url_valid_domain_chars + @"(?:[_-]|" + url_valid_domain_chars + @")*)?" + url_valid_domain_chars + @"\.)";
90 private const string url_valid_domain_name = @"(?:(?:" + url_valid_domain_chars + @"(?:-|" + url_valid_domain_chars + @")*)?" + url_valid_domain_chars + @"\.)";
91 private const string url_valid_GTLD = @"(?:(?:aero|asia|biz|cat|com|coop|edu|gov|info|int|jobs|mil|mobi|museum|name|net|org|pro|tel|travel|xxx)(?=[^0-9a-zA-Z]|$))";
92 private const string url_valid_CCTLD = @"(?:(?:ac|ad|ae|af|ag|ai|al|am|an|ao|aq|ar|as|at|au|aw|ax|az|ba|bb|bd|be|bf|bg|bh|bi|bj|bm|bn|bo|br|bs|bt|bv|bw|by|bz|ca|cc|cd|cf|cg|ch|ci|ck|cl|cm|cn|co|cr|cs|cu|cv|cx|cy|cz|dd|de|dj|dk|dm|do|dz|ec|ee|eg|eh|er|es|et|eu|fi|fj|fk|fm|fo|fr|ga|gb|gd|ge|gf|gg|gh|gi|gl|gm|gn|gp|gq|gr|gs|gt|gu|gw|gy|hk|hm|hn|hr|ht|hu|id|ie|il|im|in|io|iq|ir|is|it|je|jm|jo|jp|ke|kg|kh|ki|km|kn|kp|kr|kw|ky|kz|la|lb|lc|li|lk|lr|ls|lt|lu|lv|ly|ma|mc|md|me|mg|mh|mk|ml|mm|mn|mo|mp|mq|mr|ms|mt|mu|mv|mw|mx|my|mz|na|nc|ne|nf|ng|ni|nl|no|np|nr|nu|nz|om|pa|pe|pf|pg|ph|pk|pl|pm|pn|pr|ps|pt|pw|py|qa|re|ro|rs|ru|rw|sa|sb|sc|sd|se|sg|sh|si|sj|sk|sl|sm|sn|so|sr|ss|st|su|sv|sy|sz|tc|td|tf|tg|th|tj|tk|tl|tm|tn|to|tp|tr|tt|tv|tw|tz|ua|ug|uk|us|uy|uz|va|vc|ve|vg|vi|vn|vu|wf|ws|ye|yt|za|zm|zw)(?=[^0-9a-zA-Z]|$))";
93 private const string url_valid_punycode = @"(?:xn--[0-9a-z]+)";
94 private const string url_valid_domain = @"(?<domain>" + url_valid_subdomain + "*" + url_valid_domain_name + "(?:" + url_valid_GTLD + "|" + url_valid_CCTLD + ")|" + url_valid_punycode + ")";
95 public const string url_valid_ascii_domain = @"(?:(?:[a-z0-9" + LATIN_ACCENTS + @"]+)\.)+(?:" + url_valid_GTLD + "|" + url_valid_CCTLD + "|" + url_valid_punycode + ")";
96 public const string url_invalid_short_domain = "^" + url_valid_domain_name + url_valid_CCTLD + "$";
97 private const string url_valid_port_number = @"[0-9]+";
99 private const string url_valid_general_path_chars = @"[a-z0-9!*';:=+,.$/%#\[\]\-_~|&" + LATIN_ACCENTS + "]";
100 private const string url_balance_parens = @"(?:\(" + url_valid_general_path_chars + @"+\))";
101 private const string url_valid_path_ending_chars = @"(?:[+\-a-z0-9=_#/" + LATIN_ACCENTS + "]|" + url_balance_parens + ")";
102 private const string pth = "(?:" +
104 url_valid_general_path_chars + "*" +
105 "(?:" + url_balance_parens + url_valid_general_path_chars + "*)*" +
106 url_valid_path_ending_chars +
107 ")|(?:@" + url_valid_general_path_chars + "+/)" +
109 private const string qry = @"(?<query>\?[a-z0-9!?*'();:&=+$/%#\[\]\-_.,~|]*[a-z0-9_&=#/])?";
110 public const string rgUrl = @"(?<before>" + url_valid_preceding_chars + ")" +
111 "(?<url>(?<protocol>https?://)?" +
112 "(?<domain>" + url_valid_domain + ")" +
113 "(?::" + url_valid_port_number + ")?" +
114 "(?<path>/" + pth + "*)?" +
121 /// Twitter API のステータスページのURL
123 public const string ServiceAvailabilityStatusUrl = "https://status.io.watchmouse.com/7617";
126 /// ツイートへのパーマリンクURLを判定する正規表現
128 public static readonly Regex StatusUrlRegex = new Regex(@"https?://([^.]+\.)?twitter\.com/(#!/)?(?<ScreenName>[a-zA-Z0-9_]+)/status(es)?/(?<StatusId>[0-9]+)(/photo)?", RegexOptions.IgnoreCase);
131 /// FavstarやaclogなどTwitter関連サービスのパーマリンクURLからステータスIDを抽出する正規表現
133 public static readonly Regex ThirdPartyStatusUrlRegex = new Regex(@"https?://(?:[^.]+\.)?(?:
134 favstar\.fm/users/[a-zA-Z0-9_]+/status/ # Favstar
135 | favstar\.fm/t/ # Favstar (short)
136 | aclog\.koba789\.com/i/ # aclog
137 | frtrt\.net/solo_status\.php\?status= # RtRT
138 )(?<StatusId>[0-9]+)", RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace);
141 /// DM送信かどうかを判定する正規表現
143 public static readonly Regex DMSendTextRegex = new Regex(@"^DM? +(?<id>[a-zA-Z0-9_]+) +(?<body>.*)", RegexOptions.IgnoreCase | RegexOptions.Singleline);
145 public TwitterConfiguration Configuration { get; private set; }
147 delegate void GetIconImageDelegate(PostClass post);
148 private readonly object LockObj = new object();
149 private ISet<long> followerId = new HashSet<long>();
150 private bool _GetFollowerResult = false;
151 private long[] noRTId = new long[0];
152 private bool _GetNoRetweetResult = false;
155 private string _uname;
157 private bool _readOwnPost;
158 private List<string> _hashList = new List<string>();
160 //max_idで古い発言を取得するために保持(lists分は個別タブで管理)
161 private long minHomeTimeline = long.MaxValue;
162 private long minMentions = long.MaxValue;
163 private long minDirectmessage = long.MaxValue;
164 private long minDirectmessageSent = long.MaxValue;
166 //private FavoriteQueue favQueue;
168 private HttpTwitter twCon = new HttpTwitter();
170 //private List<PostClass> _deletemessages = new List<PostClass>();
174 this.Configuration = TwitterConfiguration.DefaultConfiguration();
177 public TwitterApiAccessLevel AccessLevel
181 return MyCommon.TwitterApiInfo.AccessLevel;
185 protected void ResetApiStatus()
187 MyCommon.TwitterApiInfo.Reset();
190 public void Authenticate(string username, string password)
192 this.ResetApiStatus();
198 res = twCon.AuthUserAndPass(username, password, ref content);
202 throw new WebApiException("Err:" + ex.Message, ex);
205 this.CheckStatusCode(res, content);
207 _uname = username.ToLowerInvariant();
208 if (SettingCommon.Instance.UserstreamStartup) this.ReconnectUserStream();
211 public string StartAuthentication()
214 this.ResetApiStatus();
217 string pinPageUrl = null;
218 var res = twCon.AuthGetRequestToken(ref pinPageUrl);
220 throw new WebApiException("Err:Failed to access auth server.");
226 throw new WebApiException("Err:Failed to access auth server.", ex);
230 public void Authenticate(string pinCode)
232 this.ResetApiStatus();
237 res = twCon.AuthGetAccessToken(pinCode);
241 throw new WebApiException("Err:Failed to access auth acc server.", ex);
244 this.CheckStatusCode(res, null);
246 _uname = Username.ToLowerInvariant();
247 if (SettingCommon.Instance.UserstreamStartup) this.ReconnectUserStream();
250 public void ClearAuthInfo()
252 Twitter.AccountState = MyCommon.ACCOUNT_STATE.Invalid;
253 this.ResetApiStatus();
254 twCon.ClearAuthInfo();
257 public void VerifyCredentials()
263 res = twCon.VerifyCredentials(ref content);
267 throw new WebApiException("Err:" + ex.Message, ex);
270 this.CheckStatusCode(res, content);
274 var user = TwitterUser.ParseJson(content);
276 this.twCon.AuthenticatedUserId = user.Id;
277 this.UpdateUserStats(user);
279 catch (SerializationException ex)
281 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
282 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
286 public void Initialize(string token, string tokenSecret, string username, long userId)
289 if (string.IsNullOrEmpty(token) || string.IsNullOrEmpty(tokenSecret) || string.IsNullOrEmpty(username))
291 Twitter.AccountState = MyCommon.ACCOUNT_STATE.Invalid;
293 this.ResetApiStatus();
294 twCon.Initialize(token, tokenSecret, username, userId);
295 _uname = username.ToLowerInvariant();
296 if (SettingCommon.Instance.UserstreamStartup) this.ReconnectUserStream();
299 public string PreProcessUrl(string orgData)
303 //var IDNConveter = new IdnMapping();
304 var href = "<a href=\"";
308 if (orgData.IndexOf(href, posl2, StringComparison.Ordinal) > -1)
312 posl1 = orgData.IndexOf(href, posl2, StringComparison.Ordinal);
313 posl1 += href.Length;
314 posl2 = orgData.IndexOf("\"", posl1, StringComparison.Ordinal);
315 urlStr = orgData.Substring(posl1, posl2 - posl1);
317 if (!urlStr.StartsWith("http://", StringComparison.Ordinal)
318 && !urlStr.StartsWith("https://", StringComparison.Ordinal)
319 && !urlStr.StartsWith("ftp://", StringComparison.Ordinal))
324 var replacedUrl = MyCommon.IDNEncode(urlStr);
325 if (replacedUrl == null) continue;
326 if (replacedUrl == urlStr) continue;
328 orgData = orgData.Replace("<a href=\"" + urlStr, "<a href=\"" + replacedUrl);
339 private string GetPlainText(string orgData)
341 return WebUtility.HtmlDecode(Regex.Replace(orgData, "(?<tagStart><a [^>]+>)(?<text>[^<]+)(?<tagEnd></a>)", "${text}"));
344 // htmlの簡易サニタイズ(詳細表示に不要なタグの除去)
346 private string SanitizeHtml(string orgdata)
348 var retdata = orgdata;
350 retdata = Regex.Replace(retdata, "<(script|object|applet|image|frameset|fieldset|legend|style).*" +
351 "</(script|object|applet|image|frameset|fieldset|legend|style)>", "", RegexOptions.IgnoreCase);
353 retdata = Regex.Replace(retdata, "<(frame|link|iframe|img)>", "", RegexOptions.IgnoreCase);
358 private string AdjustHtml(string orgData)
360 var retStr = orgData;
361 //var m = Regex.Match(retStr, "<a [^>]+>[#|#](?<1>[a-zA-Z0-9_]+)</a>");
366 // _hashList.Add("#" + m.Groups(1).Value);
370 retStr = Regex.Replace(retStr, "<a [^>]*href=\"/", "<a href=\"https://twitter.com/");
371 retStr = retStr.Replace("<a href=", "<a target=\"_self\" href=");
372 retStr = Regex.Replace(retStr, @"(\r\n?|\n)", "<br>"); // CRLF, CR, LF は全て <br> に置換する
374 //半角スペースを置換(Thanks @anis774)
378 ret = EscapeSpace(ref retStr);
381 return SanitizeHtml(retStr);
384 private bool EscapeSpace(ref string html)
386 //半角スペースを置換(Thanks @anis774)
388 for (int i = 0; i < html.Length; i++)
399 if ((!isTag) && (html[i] == ' '))
401 html = html.Remove(i, 1);
402 html = html.Insert(i, " ");
409 private struct PostInfo
411 public string CreatedAt;
414 public string UserId;
415 public PostInfo(string Created, string IdStr, string txt, string uid)
422 public bool Equals(PostInfo dst)
424 if (this.CreatedAt == dst.CreatedAt && this.Id == dst.Id && this.Text == dst.Text && this.UserId == dst.UserId)
435 static private PostInfo _prev = new PostInfo("", "", "", "");
436 private bool IsPostRestricted(TwitterStatus status)
438 var _current = new PostInfo("", "", "", "");
440 _current.CreatedAt = status.CreatedAt;
441 _current.Id = status.IdStr;
442 if (status.Text == null)
448 _current.Text = status.Text;
450 _current.UserId = status.User.IdStr;
452 if (_current.Equals(_prev))
456 _prev.CreatedAt = _current.CreatedAt;
457 _prev.Id = _current.Id;
458 _prev.Text = _current.Text;
459 _prev.UserId = _current.UserId;
464 public void PostStatus(string postStr, long? reply_to, List<long> mediaIds = null)
466 this.CheckAccountState();
468 if (mediaIds == null &&
469 Twitter.DMSendTextRegex.IsMatch(postStr))
471 SendDirectMessage(postStr);
479 res = twCon.UpdateStatus(postStr, reply_to, mediaIds, ref content);
483 throw new WebApiException("Err:" + ex.Message, ex);
486 // 投稿に成功していても404が返ることがあるらしい: https://dev.twitter.com/discussions/1213
487 if (res == HttpStatusCode.NotFound)
490 this.CheckStatusCode(res, content);
492 TwitterStatus status;
495 status = TwitterStatus.ParseJson(content);
497 catch(SerializationException ex)
499 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
500 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
504 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
505 throw new WebApiException("Err:Invalid Json!", content, ex);
508 this.UpdateUserStats(status.User);
510 if (IsPostRestricted(status))
512 throw new WebApiException("OK:Delaying?");
516 public void PostStatusWithMedia(string postStr, long? reply_to, IMediaItem item)
518 this.CheckAccountState();
524 res = twCon.UpdateStatusWithMedia(postStr, reply_to, item, ref content);
528 throw new WebApiException("Err:" + ex.Message, ex);
531 // 投稿に成功していても404が返ることがあるらしい: https://dev.twitter.com/discussions/1213
532 if (res == HttpStatusCode.NotFound)
535 this.CheckStatusCode(res, content);
537 TwitterStatus status;
540 status = TwitterStatus.ParseJson(content);
542 catch(SerializationException ex)
544 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
545 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
549 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
550 throw new WebApiException("Err:Invalid Json!", content, ex);
553 this.UpdateUserStats(status.User);
555 if (IsPostRestricted(status))
557 throw new WebApiException("OK:Delaying?");
561 public void PostStatusWithMultipleMedia(string postStr, long? reply_to, IMediaItem[] mediaItems)
563 this.CheckAccountState();
565 if (Twitter.DMSendTextRegex.IsMatch(postStr))
567 SendDirectMessage(postStr);
571 var mediaIds = new List<long>();
573 foreach (var item in mediaItems)
575 var mediaId = UploadMedia(item);
576 mediaIds.Add(mediaId);
579 if (mediaIds.Count == 0)
580 throw new WebApiException("Err:Invalid Files!");
582 PostStatus(postStr, reply_to, mediaIds);
585 public long UploadMedia(IMediaItem item)
587 this.CheckAccountState();
593 res = twCon.UploadMedia(item, ref content);
597 throw new WebApiException("Err:" + ex.Message, ex);
600 this.CheckStatusCode(res, content);
602 TwitterUploadMediaResult status;
605 status = TwitterUploadMediaResult.ParseJson(content);
607 catch (SerializationException ex)
609 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
610 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
614 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
615 throw new WebApiException("Err:Invalid Json!", content, ex);
618 return status.MediaId;
621 public void SendDirectMessage(string postStr)
623 this.CheckAccountState();
624 this.CheckAccessLevel(TwitterApiAccessLevel.ReadWriteAndDirectMessage);
626 var mc = Twitter.DMSendTextRegex.Match(postStr);
632 res = twCon.SendDirectMessage(mc.Groups["body"].Value, mc.Groups["id"].Value, ref content);
636 throw new WebApiException("Err:" + ex.Message, ex);
639 this.CheckStatusCode(res, content);
641 TwitterDirectMessage status;
644 status = TwitterDirectMessage.ParseJson(content);
646 catch(SerializationException ex)
648 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
649 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
653 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
654 throw new WebApiException("Err:Invalid Json!", content, ex);
657 this.UpdateUserStats(status.Sender);
660 public void RemoveStatus(long id)
662 this.CheckAccountState();
667 res = twCon.DestroyStatus(id);
671 throw new WebApiException("Err:" + ex.Message, ex);
674 this.CheckStatusCode(res, null);
677 public void PostRetweet(long id, bool read)
679 this.CheckAccountState();
683 var post = TabInformations.GetInstance()[id];
686 throw new WebApiException("Err:Target isn't found.");
688 if (TabInformations.GetInstance()[id].RetweetedId != null)
690 target = TabInformations.GetInstance()[id].RetweetedId.Value; //再RTの場合は元発言をRT
697 res = twCon.RetweetStatus(target, ref content);
701 throw new WebApiException("Err:" + ex.Message, ex);
704 this.CheckStatusCode(res, content);
706 TwitterStatus status;
709 status = TwitterStatus.ParseJson(content);
711 catch(SerializationException ex)
713 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
714 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
718 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
719 throw new WebApiException("Err:Invalid Json!", content, ex);
723 post = CreatePostsFromStatusData(status);
725 throw new WebApiException("Invalid Json!", content);
730 if (TabInformations.GetInstance().ContainsKey(post.StatusId))
734 if (post.RetweetedId == null)
735 throw new WebApiException("Invalid Json!", content);
741 if (_readOwnPost) post.IsRead = true;
744 TabInformations.GetInstance().AddPost(post);
747 public void RemoveDirectMessage(long id, PostClass post)
749 this.CheckAccountState();
750 this.CheckAccessLevel(TwitterApiAccessLevel.ReadWriteAndDirectMessage);
753 // _deletemessages.Add(post)
759 res = twCon.DestroyDirectMessage(id);
763 throw new WebApiException("Err:" + ex.Message, ex);
766 this.CheckStatusCode(res, null);
769 public void PostFollowCommand(string screenName)
771 this.CheckAccountState();
777 res = twCon.CreateFriendships(screenName, ref content);
781 throw new WebApiException("Err:" + ex.Message, ex);
784 this.CheckStatusCode(res, content);
787 public void PostRemoveCommand(string screenName)
789 this.CheckAccountState();
795 res = twCon.DestroyFriendships(screenName, ref content);
799 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
802 this.CheckStatusCode(res, content);
805 public void PostCreateBlock(string screenName)
807 this.CheckAccountState();
813 res = twCon.CreateBlock(screenName, ref content);
817 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
820 this.CheckStatusCode(res, content);
823 public void PostDestroyBlock(string screenName)
825 this.CheckAccountState();
831 res = twCon.DestroyBlock(screenName, ref content);
835 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
838 this.CheckStatusCode(res, content);
841 public void PostReportSpam(string screenName)
843 this.CheckAccountState();
849 res = twCon.ReportSpam(screenName, ref content);
853 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
856 this.CheckStatusCode(res, content);
859 public TwitterFriendship GetFriendshipInfo(string screenName)
861 this.CheckAccountState();
867 res = twCon.ShowFriendships(_uname, screenName, ref content);
871 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
874 this.CheckStatusCode(res, content);
878 return TwitterFriendship.ParseJson(content);
880 catch(SerializationException ex)
882 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
883 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
887 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
888 throw new WebApiException("Err:Invalid Json!", content, ex);
892 public TwitterUser GetUserInfo(string screenName)
894 this.CheckAccountState();
900 res = twCon.ShowUserInfo(screenName, ref content);
904 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
907 this.CheckStatusCode(res, content);
911 return TwitterUser.ParseJson(content);
913 catch (SerializationException ex)
915 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
916 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
920 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
921 throw new WebApiException("Err:Invalid Json!", content, ex);
925 public int GetStatus_Retweeted_Count(long StatusId)
927 this.CheckAccountState();
933 res = twCon.ShowStatuses(StatusId, ref content);
937 throw new WebApiException("Err:" + ex.Message, ex);
940 this.CheckStatusCode(res, content);
944 var status = TwitterStatus.ParseJson(content);
945 return status.RetweetCount;
947 catch (SerializationException ex)
949 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
950 throw new WebApiException("Json Parse Error(DataContractJsonSerializer)", content, ex);
954 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
955 throw new WebApiException("Invalid Json!", content, ex);
959 public void PostFavAdd(long id)
961 this.CheckAccountState();
963 //if (this.favQueue == null) this.favQueue = new FavoriteQueue(this)
965 //if (this.favQueue.Contains(id)) this.favQueue.Remove(id)
971 res = twCon.CreateFavorites(id, ref content);
975 //this.favQueue.Add(id)
976 //return "Err:->FavoriteQueue:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")";
977 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
980 this.CheckStatusCode(res, content);
982 if (!RestrictFavCheck)
985 //http://twitter.com/statuses/show/id.xml APIを発行して本文を取得
989 res = twCon.ShowStatuses(id, ref content);
993 throw new WebApiException("Err:" + ex.Message, ex);
996 this.CheckStatusCode(res, content);
998 TwitterStatus status;
1001 status = TwitterStatus.ParseJson(content);
1003 catch (SerializationException ex)
1005 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
1006 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
1008 catch (Exception ex)
1010 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
1011 throw new WebApiException("Err:Invalid Json!", content, ex);
1013 if (status.Favorited != true)
1014 throw new WebApiException("NG(Restricted?)");
1017 public void PostFavRemove(long id)
1019 this.CheckAccountState();
1021 //if (this.favQueue == null) this.favQueue = new FavoriteQueue(this)
1023 //if (this.favQueue.Contains(id))
1024 // this.favQueue.Remove(id)
1032 res = twCon.DestroyFavorites(id, ref content);
1036 throw new WebApiException("Err:" + ex.Message, ex);
1039 this.CheckStatusCode(res, content);
1042 public TwitterUser PostUpdateProfile(string name, string url, string location, string description)
1044 this.CheckAccountState();
1050 res = twCon.UpdateProfile(name, url, location, description, ref content);
1054 throw new WebApiException("Err:" + ex.Message, content, ex);
1057 this.CheckStatusCode(res, content);
1061 return TwitterUser.ParseJson(content);
1063 catch (SerializationException e)
1065 var ex = new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, e);
1066 MyCommon.TraceOut(ex);
1071 var ex = new WebApiException("Err:Invalid Json!", content, e);
1072 MyCommon.TraceOut(ex);
1077 public void PostUpdateProfileImage(string filename)
1079 this.CheckAccountState();
1085 res = twCon.UpdateProfileImage(new FileInfo(filename), ref content);
1089 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
1092 this.CheckStatusCode(res, content);
1095 public string Username
1099 return twCon.AuthenticatedUsername;
1107 return twCon.AuthenticatedUserId;
1111 public string Password
1115 return twCon.Password;
1119 private static MyCommon.ACCOUNT_STATE _accountState = MyCommon.ACCOUNT_STATE.Valid;
1120 public static MyCommon.ACCOUNT_STATE AccountState
1124 return _accountState;
1128 _accountState = value;
1132 public bool RestrictFavCheck { get; set; }
1135 public void GetTweenBinary(string strVer)
1140 if (!(new HttpVarious()).GetDataToFile("http://tween.sourceforge.jp/Tween" + strVer + ".gz?" + DateTime.Now.ToString("yyMMddHHmmss") + Environment.TickCount.ToString(),
1141 Path.Combine(MyCommon.settingPath, "TweenNew.exe")))
1143 throw new WebApiException("Err:Download failed");
1146 if (!Directory.Exists(Path.Combine(MyCommon.settingPath, "en")))
1148 Directory.CreateDirectory(Path.Combine(MyCommon.settingPath, "en"));
1150 if (!(new HttpVarious()).GetDataToFile("http://tween.sourceforge.jp/TweenResEn" + strVer + ".gz?" + DateTime.Now.ToString("yyMMddHHmmss") + Environment.TickCount.ToString(),
1151 Path.Combine(Path.Combine(MyCommon.settingPath, "en"), "Tween.resourcesNew.dll")))
1153 throw new WebApiException("Err:Download failed");
1155 //その他言語圏のリソース。取得失敗しても継続
1158 if (!Thread.CurrentThread.CurrentUICulture.IsNeutralCulture)
1160 var idx = Thread.CurrentThread.CurrentUICulture.Name.LastIndexOf('-');
1163 curCul = Thread.CurrentThread.CurrentUICulture.Name.Substring(0, idx);
1167 curCul = Thread.CurrentThread.CurrentUICulture.Name;
1172 curCul = Thread.CurrentThread.CurrentUICulture.Name;
1174 if (!string.IsNullOrEmpty(curCul) && curCul != "en" && curCul != "ja")
1176 if (!Directory.Exists(Path.Combine(MyCommon.settingPath, curCul)))
1178 Directory.CreateDirectory(Path.Combine(MyCommon.settingPath, curCul));
1180 if (!(new HttpVarious()).GetDataToFile("http://tween.sourceforge.jp/TweenRes" + curCul + strVer + ".gz?" + DateTime.Now.ToString("yyMMddHHmmss") + Environment.TickCount.ToString(),
1181 Path.Combine(Path.Combine(MyCommon.settingPath, curCul), "Tween.resourcesNew.dll")))
1183 //return "Err:Download failed";
1188 if (!Thread.CurrentThread.CurrentCulture.IsNeutralCulture)
1190 var idx = Thread.CurrentThread.CurrentCulture.Name.LastIndexOf('-');
1193 curCul2 = Thread.CurrentThread.CurrentCulture.Name.Substring(0, idx);
1197 curCul2 = Thread.CurrentThread.CurrentCulture.Name;
1202 curCul2 = Thread.CurrentThread.CurrentCulture.Name;
1204 if (!string.IsNullOrEmpty(curCul2) && curCul2 != "en" && curCul2 != curCul)
1206 if (!Directory.Exists(Path.Combine(MyCommon.settingPath, curCul2)))
1208 Directory.CreateDirectory(Path.Combine(MyCommon.settingPath, curCul2));
1210 if (!(new HttpVarious()).GetDataToFile("http://tween.sourceforge.jp/TweenRes" + curCul2 + strVer + ".gz?" + DateTime.Now.ToString("yyMMddHHmmss") + Environment.TickCount.ToString(),
1211 Path.Combine(Path.Combine(MyCommon.settingPath, curCul2), "Tween.resourcesNew.dll")))
1213 //return "Err:Download failed";
1218 if (!(new HttpVarious()).GetDataToFile("http://tween.sourceforge.jp/TweenUp3.gz?" + DateTime.Now.ToString("yyMMddHHmmss") + Environment.TickCount.ToString(),
1219 Path.Combine(MyCommon.settingPath, "TweenUp3.exe")))
1221 throw new WebApiException("Err:Download failed");
1224 if (!(new HttpVarious()).GetDataToFile("http://tween.sourceforge.jp/TweenDll" + strVer + ".gz?" + DateTime.Now.ToString("yyMMddHHmmss") + Environment.TickCount.ToString(),
1225 Path.Combine(MyCommon.settingPath, "TweenNew.XmlSerializers.dll")))
1227 throw new WebApiException("Err:Download failed");
1230 catch (Exception ex)
1232 throw new WebApiException("Err:Download failed", ex);
1237 public bool ReadOwnPost
1241 return _readOwnPost;
1245 _readOwnPost = value;
1249 public int FollowersCount { get; private set; }
1250 public int FriendsCount { get; private set; }
1251 public int StatusesCount { get; private set; }
1252 public string Location { get; private set; } = "";
1253 public string Bio { get; private set; } = "";
1255 /// <summary>ユーザーのフォロワー数などの情報を更新します</summary>
1256 private void UpdateUserStats(TwitterUser self)
1258 this.FollowersCount = self.FollowersCount;
1259 this.FriendsCount = self.FriendsCount;
1260 this.StatusesCount = self.StatusesCount;
1261 this.Location = self.Location;
1262 this.Bio = self.Description;
1266 /// 渡された取得件数がWORKERTYPEに応じた取得可能範囲に収まっているか検証する
1268 public static bool VerifyApiResultCount(MyCommon.WORKERTYPE type, int count)
1270 return count >= 20 && count <= GetMaxApiResultCount(type);
1274 /// 渡された取得件数が更新時の取得可能範囲に収まっているか検証する
1276 public static bool VerifyMoreApiResultCount(int count)
1278 return count >= 20 && count <= 200;
1282 /// 渡された取得件数が起動時の取得可能範囲に収まっているか検証する
1284 public static bool VerifyFirstApiResultCount(int count)
1286 return count >= 20 && count <= 200;
1290 /// WORKERTYPEに応じた取得可能な最大件数を取得する
1292 public static int GetMaxApiResultCount(MyCommon.WORKERTYPE type)
1294 // 参照: REST APIs - 各endpointのcountパラメータ
1295 // https://dev.twitter.com/rest/public
1298 case MyCommon.WORKERTYPE.Timeline:
1299 case MyCommon.WORKERTYPE.Reply:
1300 case MyCommon.WORKERTYPE.UserTimeline:
1301 case MyCommon.WORKERTYPE.Favorites:
1302 case MyCommon.WORKERTYPE.DirectMessegeRcv:
1303 case MyCommon.WORKERTYPE.DirectMessegeSnt:
1304 case MyCommon.WORKERTYPE.List: // 不明
1307 case MyCommon.WORKERTYPE.PublicSearch:
1311 throw new InvalidOperationException("Invalid type: " + type);
1316 /// WORKERTYPEに応じた取得件数を取得する
1318 public static int GetApiResultCount(MyCommon.WORKERTYPE type, bool more, bool startup)
1320 if (type == MyCommon.WORKERTYPE.DirectMessegeRcv ||
1321 type == MyCommon.WORKERTYPE.DirectMessegeSnt)
1326 if (SettingCommon.Instance.UseAdditionalCount)
1330 case MyCommon.WORKERTYPE.Favorites:
1331 if (SettingCommon.Instance.FavoritesCountApi != 0)
1332 return SettingCommon.Instance.FavoritesCountApi;
1334 case MyCommon.WORKERTYPE.List:
1335 if (SettingCommon.Instance.ListCountApi != 0)
1336 return SettingCommon.Instance.ListCountApi;
1338 case MyCommon.WORKERTYPE.PublicSearch:
1339 if (SettingCommon.Instance.SearchCountApi != 0)
1340 return SettingCommon.Instance.SearchCountApi;
1342 case MyCommon.WORKERTYPE.UserTimeline:
1343 if (SettingCommon.Instance.UserTimelineCountApi != 0)
1344 return SettingCommon.Instance.UserTimelineCountApi;
1347 if (more && SettingCommon.Instance.MoreCountApi != 0)
1349 return Math.Min(SettingCommon.Instance.MoreCountApi, GetMaxApiResultCount(type));
1351 if (startup && SettingCommon.Instance.FirstCountApi != 0 && type != MyCommon.WORKERTYPE.Reply)
1353 return Math.Min(SettingCommon.Instance.FirstCountApi, GetMaxApiResultCount(type));
1357 // 上記に当てはまらない場合の共通処理
1358 var count = SettingCommon.Instance.CountApi;
1360 if (type == MyCommon.WORKERTYPE.Reply)
1361 count = SettingCommon.Instance.CountApiReply;
1363 return Math.Min(count, GetMaxApiResultCount(type));
1366 public void GetTimelineApi(bool read,
1367 MyCommon.WORKERTYPE gType,
1371 this.CheckAccountState();
1375 var count = GetApiResultCount(gType, more, startup);
1379 if (gType == MyCommon.WORKERTYPE.Timeline)
1383 res = twCon.HomeTimeline(count, this.minHomeTimeline, null, ref content);
1387 res = twCon.HomeTimeline(count, null, null, ref content);
1394 res = twCon.Mentions(count, this.minMentions, null, ref content);
1398 res = twCon.Mentions(count, null, null, ref content);
1404 throw new WebApiException("Err:" + ex.Message, ex);
1407 this.CheckStatusCode(res, content);
1409 var minimumId = CreatePostsFromJson(content, gType, null, read);
1411 if (minimumId != null)
1413 if (gType == MyCommon.WORKERTYPE.Timeline)
1414 this.minHomeTimeline = minimumId.Value;
1416 this.minMentions = minimumId.Value;
1420 public void GetUserTimelineApi(bool read,
1425 this.CheckAccountState();
1429 var count = GetApiResultCount(MyCommon.WORKERTYPE.UserTimeline, more, false);
1433 if (string.IsNullOrEmpty(userName))
1435 var target = tab.User;
1436 if (string.IsNullOrEmpty(target)) return;
1438 res = twCon.UserTimeline(null, target, count, null, null, ref content);
1444 res = twCon.UserTimeline(null, userName, count, tab.OldestId, null, ref content);
1448 res = twCon.UserTimeline(null, userName, count, null, null, ref content);
1454 throw new WebApiException("Err:" + ex.Message, ex);
1457 if (res == HttpStatusCode.Unauthorized)
1458 throw new WebApiException("Err:@" + userName + "'s Tweets are protected.");
1460 this.CheckStatusCode(res, content);
1462 var minimumId = CreatePostsFromJson(content, MyCommon.WORKERTYPE.UserTimeline, tab, read);
1464 if (minimumId != null)
1465 tab.OldestId = minimumId.Value;
1468 public PostClass GetStatusApi(bool read, long id)
1470 this.CheckAccountState();
1476 res = twCon.ShowStatuses(id, ref content);
1480 throw new WebApiException("Err:" + ex.Message, ex);
1483 if (res == HttpStatusCode.Forbidden)
1484 throw new WebApiException("Err:protected user's tweet", content);
1486 this.CheckStatusCode(res, content);
1488 TwitterStatus status;
1491 status = TwitterStatus.ParseJson(content);
1493 catch(SerializationException ex)
1495 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
1496 throw new WebApiException("Json Parse Error(DataContractJsonSerializer)", content, ex);
1500 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
1501 throw new WebApiException("Invalid Json!", content, ex);
1504 var item = CreatePostsFromStatusData(status);
1506 throw new WebApiException("Err:Can't create post", content);
1509 if (item.IsMe && !read && _readOwnPost) item.IsRead = true;
1514 public void GetStatusApi(bool read, long id, TabClass tab)
1516 var post = this.GetStatusApi(read, id);
1518 //非同期アイコン取得&StatusDictionaryに追加
1519 if (tab != null && tab.IsInnerStorageTabType)
1520 tab.AddPostToInnerStorage(post);
1522 TabInformations.GetInstance().AddPost(post);
1525 private PostClass CreatePostsFromStatusData(TwitterStatus status)
1527 return CreatePostsFromStatusData(status, false);
1530 private PostClass CreatePostsFromStatusData(TwitterStatus status, bool favTweet)
1532 var post = new PostClass();
1533 TwitterEntities entities;
1536 post.StatusId = status.Id;
1537 if (status.RetweetedStatus != null)
1539 var retweeted = status.RetweetedStatus;
1541 post.CreatedAt = MyCommon.DateTimeParse(retweeted.CreatedAt);
1544 post.RetweetedId = retweeted.Id;
1546 post.TextFromApi = retweeted.Text;
1547 entities = retweeted.MergedEntities;
1548 sourceHtml = retweeted.Source;
1550 post.InReplyToStatusId = retweeted.InReplyToStatusId;
1551 post.InReplyToUser = retweeted.InReplyToScreenName;
1552 post.InReplyToUserId = status.InReplyToUserId;
1561 var tc = TabInformations.GetInstance().GetTabByType(MyCommon.TabUsageType.Favorites);
1562 post.IsFav = tc.Contains(retweeted.Id);
1565 if (retweeted.Coordinates != null)
1566 post.PostGeo = new PostClass.StatusGeo(retweeted.Coordinates.Coordinates[0], retweeted.Coordinates.Coordinates[1]);
1569 var user = retweeted.User;
1571 if (user == null || user.ScreenName == null || status.User.ScreenName == null) return null;
1573 post.UserId = user.Id;
1574 post.ScreenName = user.ScreenName;
1575 post.Nickname = user.Name.Trim();
1576 post.ImageUrl = user.ProfileImageUrlHttps;
1577 post.IsProtect = user.Protected;
1580 post.RetweetedBy = status.User.ScreenName;
1581 post.RetweetedByUserId = status.User.Id;
1582 post.IsMe = post.RetweetedBy.ToLowerInvariant().Equals(_uname);
1586 post.CreatedAt = MyCommon.DateTimeParse(status.CreatedAt);
1588 post.TextFromApi = status.Text;
1589 entities = status.MergedEntities;
1590 sourceHtml = status.Source;
1591 post.InReplyToStatusId = status.InReplyToStatusId;
1592 post.InReplyToUser = status.InReplyToScreenName;
1593 post.InReplyToUserId = status.InReplyToUserId;
1602 var tc = TabInformations.GetInstance().GetTabByType(MyCommon.TabUsageType.Favorites);
1603 post.IsFav = tc.Contains(post.StatusId) && TabInformations.GetInstance()[post.StatusId].IsFav;
1606 if (status.Coordinates != null)
1607 post.PostGeo = new PostClass.StatusGeo(status.Coordinates.Coordinates[0], status.Coordinates.Coordinates[1]);
1610 var user = status.User;
1612 if (user == null || user.ScreenName == null) return null;
1614 post.UserId = user.Id;
1615 post.ScreenName = user.ScreenName;
1616 post.Nickname = user.Name.Trim();
1617 post.ImageUrl = user.ProfileImageUrlHttps;
1618 post.IsProtect = user.Protected;
1619 post.IsMe = post.ScreenName.ToLowerInvariant().Equals(_uname);
1622 string textFromApi = post.TextFromApi;
1623 post.Text = CreateHtmlAnchor(textFromApi, post.ReplyToList, entities, post.Media);
1624 post.TextFromApi = textFromApi;
1625 post.TextFromApi = this.ReplaceTextFromApi(post.TextFromApi, entities);
1626 post.TextFromApi = WebUtility.HtmlDecode(post.TextFromApi);
1627 post.TextFromApi = post.TextFromApi.Replace("<3", "\u2661");
1629 post.QuoteStatusIds = GetQuoteTweetStatusIds(entities)
1630 .Where(x => x != post.StatusId && x != post.RetweetedId)
1631 .Distinct().ToArray();
1633 post.ExpandedUrls = entities.OfType<TwitterEntityUrl>()
1634 .Select(x => new PostClass.ExpandedUrlInfo(x.Url, x.ExpandedUrl))
1638 var source = ParseSource(sourceHtml);
1639 post.Source = source.Item1;
1640 post.SourceUri = source.Item2;
1642 post.IsReply = post.ReplyToList.Contains(_uname);
1643 post.IsExcludeReply = false;
1651 if (followerId.Count > 0) post.IsOwl = !followerId.Contains(post.UserId);
1659 /// ツイートに含まれる引用ツイートのURLからステータスIDを抽出
1661 public static IEnumerable<long> GetQuoteTweetStatusIds(IEnumerable<TwitterEntity> entities)
1663 var urls = entities.OfType<TwitterEntityUrl>().Select(x => x.ExpandedUrl);
1665 return GetQuoteTweetStatusIds(urls);
1668 public static IEnumerable<long> GetQuoteTweetStatusIds(IEnumerable<string> urls)
1670 foreach (var url in urls)
1672 var match = Twitter.StatusUrlRegex.Match(url);
1676 if (long.TryParse(match.Groups["StatusId"].Value, out statusId))
1677 yield return statusId;
1682 private long? CreatePostsFromJson(string content, MyCommon.WORKERTYPE gType, TabClass tab, bool read)
1684 TwitterStatus[] items;
1687 items = TwitterStatus.ParseJsonArray(content);
1689 catch(SerializationException ex)
1691 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
1692 throw new WebApiException("Json Parse Error(DataContractJsonSerializer)", content, ex);
1696 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
1697 throw new WebApiException("Invalid Json!", content, ex);
1700 long? minimumId = null;
1702 foreach (var status in items)
1704 PostClass post = null;
1705 post = CreatePostsFromStatusData(status);
1706 if (post == null) continue;
1708 if (minimumId == null || minimumId.Value > post.StatusId)
1709 minimumId = post.StatusId;
1716 if (TabInformations.GetInstance().ContainsKey(post.StatusId)) continue;
1720 if (tab.Contains(post.StatusId)) continue;
1725 if (gType != MyCommon.WORKERTYPE.UserTimeline &&
1726 post.RetweetedByUserId != null && this.noRTId.Contains(post.RetweetedByUserId.Value)) continue;
1729 if (post.IsMe && !read && _readOwnPost) post.IsRead = true;
1731 //非同期アイコン取得&StatusDictionaryに追加
1732 if (tab != null && tab.IsInnerStorageTabType)
1733 tab.AddPostToInnerStorage(post);
1735 TabInformations.GetInstance().AddPost(post);
1741 private long? CreatePostsFromSearchJson(string content, TabClass tab, bool read, int count, bool more)
1743 TwitterSearchResult items;
1746 items = TwitterSearchResult.ParseJson(content);
1748 catch (SerializationException ex)
1750 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
1751 throw new WebApiException("Json Parse Error(DataContractJsonSerializer)", content, ex);
1753 catch (Exception ex)
1755 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
1756 throw new WebApiException("Invalid Json!", content, ex);
1759 long? minimumId = null;
1761 foreach (var result in items.Statuses)
1763 PostClass post = null;
1764 post = CreatePostsFromStatusData(result);
1768 // Search API は相変わらずぶっ壊れたデータを返すことがあるため、必要なデータが欠如しているものは取得し直す
1771 post = this.GetStatusApi(read, result.Id);
1773 catch (WebApiException)
1779 if (minimumId == null || minimumId.Value > post.StatusId)
1780 minimumId = post.StatusId;
1782 if (!more && post.StatusId > tab.SinceId) tab.SinceId = post.StatusId;
1788 if (TabInformations.GetInstance().ContainsKey(post.StatusId)) continue;
1792 if (tab.Contains(post.StatusId)) continue;
1797 if ((post.IsMe && !read) && this._readOwnPost) post.IsRead = true;
1799 //非同期アイコン取得&StatusDictionaryに追加
1800 if (tab != null && tab.IsInnerStorageTabType)
1801 tab.AddPostToInnerStorage(post);
1803 TabInformations.GetInstance().AddPost(post);
1809 private void CreateFavoritePostsFromJson(string content, bool read)
1811 TwitterStatus[] item;
1814 item = TwitterStatus.ParseJsonArray(content);
1816 catch (SerializationException ex)
1818 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
1819 throw new WebApiException("Json Parse Error(DataContractJsonSerializer)", content, ex);
1821 catch (Exception ex)
1823 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
1824 throw new WebApiException("Invalid Json!", content, ex);
1827 var favTab = TabInformations.GetInstance().GetTabByType(MyCommon.TabUsageType.Favorites);
1829 foreach (var status in item)
1834 if (favTab.Contains(status.Id)) continue;
1837 var post = CreatePostsFromStatusData(status, true);
1838 if (post == null) continue;
1842 TabInformations.GetInstance().AddPost(post);
1846 public void GetListStatus(bool read,
1853 var count = GetApiResultCount(MyCommon.WORKERTYPE.List, more, startup);
1859 res = twCon.GetListsStatuses(tab.ListInfo.UserId, tab.ListInfo.Id, count, tab.OldestId, null, SettingCommon.Instance.IsListsIncludeRts, ref content);
1863 res = twCon.GetListsStatuses(tab.ListInfo.UserId, tab.ListInfo.Id, count, null, null, SettingCommon.Instance.IsListsIncludeRts, ref content);
1868 throw new WebApiException("Err:" + ex.Message, ex);
1871 this.CheckStatusCode(res, content);
1873 var minimumId = CreatePostsFromJson(content, MyCommon.WORKERTYPE.List, tab, read);
1875 if (minimumId != null)
1876 tab.OldestId = minimumId.Value;
1880 /// startStatusId からリプライ先の発言を辿る。発言は posts 以外からは検索しない。
1882 /// <returns>posts の中から検索されたリプライチェインの末端</returns>
1883 internal static PostClass FindTopOfReplyChain(IDictionary<Int64, PostClass> posts, Int64 startStatusId)
1885 if (!posts.ContainsKey(startStatusId))
1886 throw new ArgumentException("startStatusId (" + startStatusId + ") が posts の中から見つかりませんでした。", nameof(startStatusId));
1888 var nextPost = posts[startStatusId];
1889 while (nextPost.InReplyToStatusId != null)
1891 if (!posts.ContainsKey(nextPost.InReplyToStatusId.Value))
1893 nextPost = posts[nextPost.InReplyToStatusId.Value];
1899 public void GetRelatedResult(bool read, TabClass tab)
1901 var relPosts = new Dictionary<Int64, PostClass>();
1902 if (tab.RelationTargetPost.TextFromApi.Contains("@") && tab.RelationTargetPost.InReplyToStatusId == null)
1905 var p = TabInformations.GetInstance()[tab.RelationTargetPost.StatusId];
1906 if (p != null && p.InReplyToStatusId != null)
1908 tab.RelationTargetPost = p;
1912 p = this.GetStatusApi(read, tab.RelationTargetPost.StatusId);
1913 tab.RelationTargetPost = p;
1916 relPosts.Add(tab.RelationTargetPost.StatusId, tab.RelationTargetPost);
1918 Exception lastException = null;
1920 // in_reply_to_status_id を使用してリプライチェインを辿る
1921 var nextPost = FindTopOfReplyChain(relPosts, tab.RelationTargetPost.StatusId);
1923 while (nextPost.InReplyToStatusId != null && loopCount++ <= 20)
1925 var inReplyToId = nextPost.InReplyToStatusId.Value;
1927 var inReplyToPost = TabInformations.GetInstance()[inReplyToId];
1928 if (inReplyToPost == null)
1932 inReplyToPost = this.GetStatusApi(read, inReplyToId);
1934 catch (WebApiException ex)
1941 relPosts.Add(inReplyToPost.StatusId, inReplyToPost);
1943 nextPost = FindTopOfReplyChain(relPosts, nextPost.StatusId);
1946 //MRTとかに対応のためツイート内にあるツイートを指すURLを取り込む
1947 var text = tab.RelationTargetPost.Text;
1948 var ma = Twitter.StatusUrlRegex.Matches(text).Cast<Match>()
1949 .Concat(Twitter.ThirdPartyStatusUrlRegex.Matches(text).Cast<Match>());
1950 foreach (var _match in ma)
1953 if (Int64.TryParse(_match.Groups["StatusId"].Value, out _statusId))
1955 if (relPosts.ContainsKey(_statusId))
1958 var p = TabInformations.GetInstance()[_statusId];
1963 p = this.GetStatusApi(read, _statusId);
1965 catch (WebApiException ex)
1973 relPosts.Add(p.StatusId, p);
1977 relPosts.Values.ToList().ForEach(p =>
1979 if (p.IsMe && !read && this._readOwnPost)
1984 tab.AddPostToInnerStorage(p);
1987 if (lastException != null)
1988 throw new WebApiException(lastException.Message, lastException);
1991 public void GetSearch(bool read,
1997 var count = GetApiResultCount(MyCommon.WORKERTYPE.PublicSearch, more, false);
1999 long? sinceId = null;
2002 maxId = tab.OldestId - 1;
2006 sinceId = tab.SinceId;
2011 // TODO:一時的に40>100件に 件数変更UI作成の必要あり
2012 res = twCon.Search(tab.SearchWords, tab.SearchLang, count, maxId, sinceId, ref content);
2016 throw new WebApiException("Err:" + ex.Message, ex);
2020 case HttpStatusCode.BadRequest:
2021 throw new WebApiException("Invalid query", content);
2022 case HttpStatusCode.NotFound:
2023 throw new WebApiException("Invalid query", content);
2024 case HttpStatusCode.PaymentRequired: //API Documentには420と書いてあるが、該当コードがないので402にしてある
2025 throw new WebApiException("Search API Limit?", content);
2026 case HttpStatusCode.OK:
2029 throw new WebApiException("Err:" + res.ToString() + "(" + MethodBase.GetCurrentMethod().Name + ")", content);
2032 if (!TabInformations.GetInstance().ContainsTab(tab))
2035 var minimumId = this.CreatePostsFromSearchJson(content, tab, read, count, more);
2037 if (minimumId != null)
2038 tab.OldestId = minimumId.Value;
2041 private void CreateDirectMessagesFromJson(string content, MyCommon.WORKERTYPE gType, bool read)
2043 TwitterDirectMessage[] item;
2046 if (gType == MyCommon.WORKERTYPE.UserStream)
2048 item = new[] { TwitterStreamEventDirectMessage.ParseJson(content).DirectMessage };
2052 item = TwitterDirectMessage.ParseJsonArray(content);
2055 catch(SerializationException ex)
2057 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
2058 throw new WebApiException("Json Parse Error(DataContractJsonSerializer)", content, ex);
2062 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
2063 throw new WebApiException("Invalid Json!", content, ex);
2066 foreach (var message in item)
2068 var post = new PostClass();
2071 post.StatusId = message.Id;
2072 if (gType != MyCommon.WORKERTYPE.UserStream)
2074 if (gType == MyCommon.WORKERTYPE.DirectMessegeRcv)
2076 if (minDirectmessage > post.StatusId) minDirectmessage = post.StatusId;
2080 if (minDirectmessageSent > post.StatusId) minDirectmessageSent = post.StatusId;
2087 if (TabInformations.GetInstance().GetTabByType(MyCommon.TabUsageType.DirectMessage).Contains(post.StatusId)) continue;
2091 post.CreatedAt = MyCommon.DateTimeParse(message.CreatedAt);
2093 var textFromApi = message.Text;
2095 post.Text = CreateHtmlAnchor(textFromApi, post.ReplyToList, message.Entities, post.Media);
2096 post.TextFromApi = this.ReplaceTextFromApi(textFromApi, message.Entities);
2097 post.TextFromApi = WebUtility.HtmlDecode(post.TextFromApi);
2098 post.TextFromApi = post.TextFromApi.Replace("<3", "\u2661");
2101 post.QuoteStatusIds = GetQuoteTweetStatusIds(message.Entities).Distinct().ToArray();
2103 post.ExpandedUrls = message.Entities.OfType<TwitterEntityUrl>()
2104 .Select(x => new PostClass.ExpandedUrlInfo(x.Url, x.ExpandedUrl))
2109 if (gType == MyCommon.WORKERTYPE.UserStream)
2111 if (twCon.AuthenticatedUsername.Equals(message.Recipient.ScreenName, StringComparison.CurrentCultureIgnoreCase))
2113 user = message.Sender;
2119 user = message.Recipient;
2126 if (gType == MyCommon.WORKERTYPE.DirectMessegeRcv)
2128 user = message.Sender;
2134 user = message.Recipient;
2140 post.UserId = user.Id;
2141 post.ScreenName = user.ScreenName;
2142 post.Nickname = user.Name.Trim();
2143 post.ImageUrl = user.ProfileImageUrlHttps;
2144 post.IsProtect = user.Protected;
2148 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
2149 MessageBox.Show("Parse Error(CreateDirectMessagesFromJson)");
2154 if (post.IsMe && !read && _readOwnPost) post.IsRead = true;
2155 post.IsReply = false;
2156 post.IsExcludeReply = false;
2159 var dmTab = TabInformations.GetInstance().GetTabByType(MyCommon.TabUsageType.DirectMessage);
2160 dmTab.AddPostToInnerStorage(post);
2164 public void GetDirectMessageApi(bool read,
2165 MyCommon.WORKERTYPE gType,
2168 this.CheckAccountState();
2169 this.CheckAccessLevel(TwitterApiAccessLevel.ReadWriteAndDirectMessage);
2173 var count = GetApiResultCount(gType, more, false);
2177 if (gType == MyCommon.WORKERTYPE.DirectMessegeRcv)
2181 res = twCon.DirectMessages(count, minDirectmessage, null, ref content);
2185 res = twCon.DirectMessages(count, null, null, ref content);
2192 res = twCon.DirectMessagesSent(count, minDirectmessageSent, null, ref content);
2196 res = twCon.DirectMessagesSent(count, null, null, ref content);
2202 throw new WebApiException("Err:" + ex.Message, ex);
2205 this.CheckStatusCode(res, content);
2207 CreateDirectMessagesFromJson(content, gType, read);
2210 public void GetFavoritesApi(bool read,
2213 this.CheckAccountState();
2217 var count = GetApiResultCount(MyCommon.WORKERTYPE.Favorites, more, false);
2221 res = twCon.Favorites(count, ref content);
2225 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
2228 this.CheckStatusCode(res, content);
2230 CreateFavoritePostsFromJson(content, read);
2233 private string ReplaceTextFromApi(string text, TwitterEntities entities)
2235 if (entities != null)
2237 if (entities.Urls != null)
2239 foreach (var m in entities.Urls)
2241 if (!string.IsNullOrEmpty(m.DisplayUrl)) text = text.Replace(m.Url, m.DisplayUrl);
2244 if (entities.Media != null)
2246 foreach (var m in entities.Media)
2248 if (!string.IsNullOrEmpty(m.DisplayUrl)) text = text.Replace(m.Url, m.DisplayUrl);
2258 /// <exception cref="WebApiException"/>
2259 public void RefreshFollowerIds()
2261 if (MyCommon._endingFlag) return;
2264 var newFollowerIds = new HashSet<long>();
2267 var ret = this.GetFollowerIdsApi(ref cursor);
2268 newFollowerIds.UnionWith(ret.Ids);
2269 cursor = ret.NextCursor;
2270 } while (cursor != 0);
2272 this.followerId = newFollowerIds;
2273 TabInformations.GetInstance().RefreshOwl(this.followerId);
2275 this._GetFollowerResult = true;
2278 public bool GetFollowersSuccess
2282 return _GetFollowerResult;
2286 private TwitterIds GetFollowerIdsApi(ref long cursor)
2288 this.CheckAccountState();
2294 res = twCon.FollowerIds(cursor, ref content);
2298 throw new WebApiException("Err:" + e.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", e);
2301 this.CheckStatusCode(res, content);
2305 var ret = TwitterIds.ParseJson(content);
2307 if (ret.Ids == null)
2309 var ex = new WebApiException("Err: ret.id == null (GetFollowerIdsApi)", content);
2310 MyCommon.ExceptionOut(ex);
2316 catch(SerializationException e)
2318 var ex = new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, e);
2319 MyCommon.TraceOut(ex);
2324 var ex = new WebApiException("Err:Invalid Json!", content, e);
2325 MyCommon.TraceOut(ex);
2331 /// RT 非表示ユーザーを更新します
2333 /// <exception cref="WebApiException"/>
2334 public void RefreshNoRetweetIds()
2336 if (MyCommon._endingFlag) return;
2338 this.noRTId = this.NoRetweetIdsApi();
2340 this._GetNoRetweetResult = true;
2343 private long[] NoRetweetIdsApi()
2345 this.CheckAccountState();
2351 res = twCon.NoRetweetIds(ref content);
2355 throw new WebApiException("Err:" + e.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", e);
2358 this.CheckStatusCode(res, content);
2362 return MyCommon.CreateDataFromJson<long[]>(content);
2364 catch(SerializationException e)
2366 var ex = new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, e);
2367 MyCommon.TraceOut(ex);
2372 var ex = new WebApiException("Err:Invalid Json!", content, e);
2373 MyCommon.TraceOut(ex);
2378 public bool GetNoRetweetSuccess
2382 return _GetNoRetweetResult;
2387 /// t.co の文字列長などの設定情報を更新します
2389 /// <exception cref="WebApiException"/>
2390 public void RefreshConfiguration()
2392 this.Configuration = this.ConfigurationApi();
2395 private TwitterConfiguration ConfigurationApi()
2401 res = twCon.GetConfiguration(ref content);
2405 throw new WebApiException("Err:" + e.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", e);
2408 this.CheckStatusCode(res, content);
2412 return TwitterConfiguration.ParseJson(content);
2414 catch(SerializationException e)
2416 var ex = new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, e);
2417 MyCommon.TraceOut(ex);
2422 var ex = new WebApiException("Err:Invalid Json!", content, e);
2423 MyCommon.TraceOut(ex);
2428 public void GetListsApi()
2430 this.CheckAccountState();
2433 IEnumerable<ListElement> lists;
2438 res = twCon.GetLists(this.Username, ref content);
2440 catch (Exception ex)
2442 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
2445 this.CheckStatusCode(res, content);
2449 lists = TwitterList.ParseJsonArray(content)
2450 .Select(x => new ListElement(x, this));
2452 catch (SerializationException ex)
2454 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
2455 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
2457 catch (Exception ex)
2459 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
2460 throw new WebApiException("Err:Invalid Json!", content, ex);
2465 res = twCon.GetListsSubscriptions(this.Username, ref content);
2467 catch (Exception ex)
2469 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
2472 this.CheckStatusCode(res, content);
2476 lists = lists.Concat(TwitterList.ParseJsonArray(content)
2477 .Select(x => new ListElement(x, this)));
2479 catch (SerializationException ex)
2481 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
2482 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
2484 catch (Exception ex)
2486 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
2487 throw new WebApiException("Err:Invalid Json!", content, ex);
2490 TabInformations.GetInstance().SubscribableLists = lists.ToList();
2493 public void DeleteList(string list_id)
2500 res = twCon.DeleteListID(this.Username, list_id, ref content);
2504 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
2507 this.CheckStatusCode(res, content);
2510 public ListElement EditList(string list_id, string new_name, bool isPrivate, string description)
2517 res = twCon.UpdateListID(this.Username, list_id, new_name, isPrivate, description, ref content);
2521 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
2524 this.CheckStatusCode(res, content);
2528 var le = TwitterList.ParseJson(content);
2529 return new ListElement(le, this);
2531 catch(SerializationException ex)
2533 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
2534 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
2538 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
2539 throw new WebApiException("Err:Invalid Json!", content, ex);
2543 public long GetListMembers(string list_id, List<UserInfo> lists, long cursor)
2545 this.CheckAccountState();
2551 res = twCon.GetListMembers(this.Username, list_id, cursor, ref content);
2555 throw new WebApiException("Err:" + ex.Message);
2558 this.CheckStatusCode(res, content);
2562 var users = TwitterUsers.ParseJson(content);
2563 Array.ForEach<TwitterUser>(
2565 u => lists.Add(new UserInfo(u)));
2567 return users.NextCursor;
2569 catch(SerializationException ex)
2571 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
2572 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
2576 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
2577 throw new WebApiException("Err:Invalid Json!", content, ex);
2581 public void CreateListApi(string listName, bool isPrivate, string description)
2583 this.CheckAccountState();
2589 res = twCon.CreateLists(listName, isPrivate, description, ref content);
2593 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
2596 this.CheckStatusCode(res, content);
2600 var le = TwitterList.ParseJson(content);
2601 TabInformations.GetInstance().SubscribableLists.Add(new ListElement(le, this));
2603 catch(SerializationException ex)
2605 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
2606 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
2610 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
2611 throw new WebApiException("Err:Invalid Json!", content, ex);
2615 public bool ContainsUserAtList(string listId, string user)
2617 this.CheckAccountState();
2624 res = this.twCon.ShowListMember(listId, user, ref content);
2628 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
2631 if (res == HttpStatusCode.NotFound)
2636 this.CheckStatusCode(res, content);
2640 TwitterUser.ParseJson(content);
2649 public void AddUserToList(string listId, string user)
2656 res = twCon.CreateListMembers(listId, user, ref content);
2660 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
2663 this.CheckStatusCode(res, content);
2666 public void RemoveUserToList(string listId, string user)
2673 res = twCon.DeleteListMembers(listId, user, ref content);
2677 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
2680 this.CheckStatusCode(res, content);
2683 public string CreateHtmlAnchor(string text, List<string> AtList, TwitterEntities entities, List<MediaInfo> media)
2685 if (entities != null)
2687 if (entities.Hashtags != null)
2691 this._hashList.AddRange(entities.Hashtags.Select(x => "#" + x.Text));
2694 if (entities.UserMentions != null)
2696 foreach (var ent in entities.UserMentions)
2698 var screenName = ent.ScreenName.ToLowerInvariant();
2699 if (!AtList.Contains(screenName))
2700 AtList.Add(screenName);
2703 if (entities.Media != null)
2707 foreach (var ent in entities.Media)
2709 if (!media.Any(x => x.Url == ent.MediaUrl))
2711 if (ent.VideoInfo != null &&
2712 ent.Type == "animated_gif" || ent.Type == "video")
2714 //var videoUrl = ent.VideoInfo.Variants
2715 // .Where(v => v.ContentType == "video/mp4")
2716 // .OrderByDescending(v => v.Bitrate)
2717 // .Select(v => v.Url).FirstOrDefault();
2718 media.Add(new MediaInfo(ent.MediaUrl, ent.ExpandedUrl));
2721 media.Add(new MediaInfo(ent.MediaUrl));
2728 // PostClass.ExpandedUrlInfo を使用して非同期に URL 展開を行うためここでは expanded_url を使用しない
2729 text = TweetFormatter.AutoLinkHtml(text, entities, keepTco: true);
2731 text = Regex.Replace(text, "(^|[^a-zA-Z0-9_/&##@@>=.~])(sm|nm)([0-9]{1,10})", "$1<a href=\"http://www.nicovideo.jp/watch/$2$3\">$2$3</a>");
2732 text = PreProcessUrl(text); //IDN置換
2737 private static readonly Uri SourceUriBase = new Uri("https://twitter.com/");
2740 /// Twitter APIから得たHTML形式のsource文字列を分析し、source名とURLに分離します
2742 public static Tuple<string, Uri> ParseSource(string sourceHtml)
2744 if (string.IsNullOrEmpty(sourceHtml))
2745 return Tuple.Create<string, Uri>("", null);
2750 // sourceHtmlの例: <a href="http://twitter.com" rel="nofollow">Twitter Web Client</a>
2752 var match = Regex.Match(sourceHtml, "^<a href=\"(?<uri>.+?)\".*?>(?<text>.+)</a>$", RegexOptions.IgnoreCase);
2755 sourceText = WebUtility.HtmlDecode(match.Groups["text"].Value);
2758 var uriStr = WebUtility.HtmlDecode(match.Groups["uri"].Value);
2759 sourceUri = new Uri(SourceUriBase, uriStr);
2761 catch (UriFormatException)
2768 sourceText = WebUtility.HtmlDecode(sourceHtml);
2772 return Tuple.Create(sourceText, sourceUri);
2775 public TwitterApiStatus GetInfoApi()
2777 if (Twitter.AccountState != MyCommon.ACCOUNT_STATE.Valid) return null;
2779 if (MyCommon._endingFlag) return null;
2785 res = twCon.RateLimitStatus(ref content);
2789 this.ResetApiStatus();
2793 this.CheckStatusCode(res, content);
2797 MyCommon.TwitterApiInfo.UpdateFromJson(content);
2798 return MyCommon.TwitterApiInfo;
2800 catch (Exception ex)
2802 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
2803 MyCommon.TwitterApiInfo.Reset();
2809 /// ブロック中のユーザーを更新します
2811 /// <exception cref="WebApiException"/>
2812 public void RefreshBlockIds()
2814 if (MyCommon._endingFlag) return;
2817 var newBlockIds = new HashSet<long>();
2820 var ret = this.GetBlockIdsApi(cursor);
2821 newBlockIds.UnionWith(ret.Ids);
2822 cursor = ret.NextCursor;
2823 } while (cursor != 0);
2825 newBlockIds.Remove(this.UserId); // 元のソースにあったので一応残しておく
2827 TabInformations.GetInstance().BlockIds = newBlockIds;
2830 public TwitterIds GetBlockIdsApi(long cursor)
2832 this.CheckAccountState();
2838 res = twCon.GetBlockUserIds(ref content, cursor);
2842 throw new WebApiException("Err:" + e.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", e);
2845 this.CheckStatusCode(res, content);
2849 return TwitterIds.ParseJson(content);
2851 catch(SerializationException e)
2853 var ex = new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, e);
2854 MyCommon.TraceOut(ex);
2859 var ex = new WebApiException("Err:Invalid Json!", content, e);
2860 MyCommon.TraceOut(ex);
2866 /// ミュート中のユーザーIDを更新します
2868 /// <exception cref="WebApiException"/>
2869 public async Task RefreshMuteUserIdsAsync()
2871 if (MyCommon._endingFlag) return;
2873 var ids = await TwitterIds.GetAllItemsAsync(this.GetMuteUserIdsApiAsync)
2874 .ConfigureAwait(false);
2876 TabInformations.GetInstance().MuteUserIds = new HashSet<long>(ids);
2879 public async Task<TwitterIds> GetMuteUserIdsApiAsync(long cursor)
2885 var res = await Task.Run(() => twCon.GetMuteUserIds(ref content, cursor))
2886 .ConfigureAwait(false);
2888 this.CheckStatusCode(res, content);
2890 return TwitterIds.ParseJson(content);
2892 catch (WebException ex)
2894 var ex2 = new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", content, ex);
2895 MyCommon.TraceOut(ex2);
2898 catch (SerializationException ex)
2900 var ex2 = new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
2901 MyCommon.TraceOut(ex2);
2906 public string[] GetHashList()
2911 hashArray = _hashList.ToArray();
2917 public string AccessToken
2921 return twCon.AccessToken;
2925 public string AccessTokenSecret
2929 return twCon.AccessTokenSecret;
2933 private void CheckAccountState()
2935 if (Twitter.AccountState != MyCommon.ACCOUNT_STATE.Valid)
2936 throw new WebApiException("Auth error. Check your account");
2939 private void CheckAccessLevel(TwitterApiAccessLevel accessLevelFlags)
2941 if (!this.AccessLevel.HasFlag(accessLevelFlags))
2942 throw new WebApiException("Auth Err:try to re-authorization.");
2945 private void CheckStatusCode(HttpStatusCode httpStatus, string responseText,
2946 [CallerMemberName] string callerMethodName = "")
2948 if (httpStatus == HttpStatusCode.OK)
2950 Twitter.AccountState = MyCommon.ACCOUNT_STATE.Valid;
2954 if (string.IsNullOrWhiteSpace(responseText))
2956 if (httpStatus == HttpStatusCode.Unauthorized)
2957 Twitter.AccountState = MyCommon.ACCOUNT_STATE.Invalid;
2959 throw new WebApiException("Err:" + httpStatus + "(" + callerMethodName + ")");
2964 var errors = TwitterError.ParseJson(responseText).Errors;
2965 if (errors == null || !errors.Any())
2967 throw new WebApiException("Err:" + httpStatus + "(" + callerMethodName + ")", responseText);
2970 foreach (var error in errors)
2972 if (error.Code == TwitterErrorCode.InvalidToken ||
2973 error.Code == TwitterErrorCode.SuspendedAccount)
2975 Twitter.AccountState = MyCommon.ACCOUNT_STATE.Invalid;
2979 throw new WebApiException("Err:" + string.Join(",", errors.Select(x => x.ToString())) + "(" + callerMethodName + ")", responseText);
2981 catch (SerializationException) { }
2983 throw new WebApiException("Err:" + httpStatus + "(" + callerMethodName + ")", responseText);
2986 public int GetTextLengthRemain(string postText)
2988 var matchDm = Twitter.DMSendTextRegex.Match(postText);
2989 if (matchDm.Success)
2990 return this.GetTextLengthRemainInternal(matchDm.Groups["body"].Value, isDm: true);
2992 return this.GetTextLengthRemainInternal(postText, isDm: false);
2995 private int GetTextLengthRemainInternal(string postText, bool isDm)
3000 while (pos < postText.Length)
3004 if (char.IsSurrogatePair(postText, pos))
3005 pos += 2; // サロゲートペアの場合は2文字分進める
3010 var urls = TweetExtractor.ExtractUrls(postText);
3011 foreach (var url in urls)
3013 var shortUrlLength = url.StartsWith("https://", StringComparison.OrdinalIgnoreCase)
3014 ? this.Configuration.ShortUrlLengthHttps
3015 : this.Configuration.ShortUrlLength;
3017 textLength += shortUrlLength - url.Length;
3021 return this.Configuration.DmTextCharacterLimit - textLength;
3023 return 140 - textLength;
3027 #region "UserStream"
3028 private string trackWord_ = "";
3029 public string TrackWord
3040 private bool allAtReply_ = false;
3041 public bool AllAtReply
3049 allAtReply_ = value;
3053 public event EventHandler NewPostFromStream;
3054 public event EventHandler UserStreamStarted;
3055 public event EventHandler UserStreamStopped;
3056 public event EventHandler<PostDeletedEventArgs> PostDeleted;
3057 public event EventHandler<UserStreamEventReceivedEventArgs> UserStreamEventReceived;
3058 private DateTime _lastUserstreamDataReceived;
3059 private TwitterUserstream userStream;
3061 public class FormattedEvent
3063 public MyCommon.EVENTTYPE Eventtype { get; set; }
3064 public DateTime CreatedAt { get; set; }
3065 public string Event { get; set; }
3066 public string Username { get; set; }
3067 public string Target { get; set; }
3068 public Int64 Id { get; set; }
3069 public bool IsMe { get; set; }
3072 public List<FormattedEvent> storedEvent_ = new List<FormattedEvent>();
3073 public List<FormattedEvent> StoredEvent
3077 return storedEvent_;
3081 storedEvent_ = value;
3085 private readonly IReadOnlyDictionary<string, MyCommon.EVENTTYPE> eventTable = new Dictionary<string, MyCommon.EVENTTYPE>
3087 ["favorite"] = MyCommon.EVENTTYPE.Favorite,
3088 ["unfavorite"] = MyCommon.EVENTTYPE.Unfavorite,
3089 ["follow"] = MyCommon.EVENTTYPE.Follow,
3090 ["list_member_added"] = MyCommon.EVENTTYPE.ListMemberAdded,
3091 ["list_member_removed"] = MyCommon.EVENTTYPE.ListMemberRemoved,
3092 ["block"] = MyCommon.EVENTTYPE.Block,
3093 ["unblock"] = MyCommon.EVENTTYPE.Unblock,
3094 ["user_update"] = MyCommon.EVENTTYPE.UserUpdate,
3095 ["deleted"] = MyCommon.EVENTTYPE.Deleted,
3096 ["list_created"] = MyCommon.EVENTTYPE.ListCreated,
3097 ["list_destroyed"] = MyCommon.EVENTTYPE.ListDestroyed,
3098 ["list_updated"] = MyCommon.EVENTTYPE.ListUpdated,
3099 ["unfollow"] = MyCommon.EVENTTYPE.Unfollow,
3100 ["list_user_subscribed"] = MyCommon.EVENTTYPE.ListUserSubscribed,
3101 ["list_user_unsubscribed"] = MyCommon.EVENTTYPE.ListUserUnsubscribed,
3102 ["mute"] = MyCommon.EVENTTYPE.Mute,
3103 ["unmute"] = MyCommon.EVENTTYPE.Unmute,
3104 ["quoted_tweet"] = MyCommon.EVENTTYPE.QuotedTweet,
3107 public bool IsUserstreamDataReceived
3111 return DateTime.Now.Subtract(this._lastUserstreamDataReceived).TotalSeconds < 31;
3115 private void userStream_StatusArrived(string line)
3117 this._lastUserstreamDataReceived = DateTime.Now;
3118 if (string.IsNullOrEmpty(line)) return;
3120 if (line.First() != '{' || line.Last() != '}')
3122 MyCommon.TraceOut("Invalid JSON (StatusArrived):" + Environment.NewLine + line);
3130 using (var jsonReader = JsonReaderWriterFactory.CreateJsonReader(Encoding.UTF8.GetBytes(line), XmlDictionaryReaderQuotas.Max))
3132 var xElm = XElement.Load(jsonReader);
3133 if (xElm.Element("friends") != null)
3135 Debug.WriteLine("friends");
3138 else if (xElm.Element("delete") != null)
3140 Debug.WriteLine("delete");
3143 if ((idElm = xElm.Element("delete").Element("direct_message")?.Element("id")) != null)
3146 long.TryParse(idElm.Value, out id);
3148 this.PostDeleted?.Invoke(this, new PostDeletedEventArgs(id));
3150 else if ((idElm = xElm.Element("delete").Element("status")?.Element("id")) != null)
3153 long.TryParse(idElm.Value, out id);
3155 this.PostDeleted?.Invoke(this, new PostDeletedEventArgs(id));
3159 MyCommon.TraceOut("delete:" + line);
3162 for (int i = this.StoredEvent.Count - 1; i >= 0; i--)
3164 var sEvt = this.StoredEvent[i];
3165 if (sEvt.Id == id && (sEvt.Event == "favorite" || sEvt.Event == "unfavorite"))
3167 this.StoredEvent.RemoveAt(i);
3172 else if (xElm.Element("limit") != null)
3174 Debug.WriteLine(line);
3177 else if (xElm.Element("event") != null)
3179 Debug.WriteLine("event: " + xElm.Element("event").Value);
3180 CreateEventFromJson(line);
3183 else if (xElm.Element("direct_message") != null)
3185 Debug.WriteLine("direct_message");
3188 else if (xElm.Element("retweeted_status") != null)
3190 var sourceUserId = xElm.XPathSelectElement("/user/id_str").Value;
3191 var targetUserId = xElm.XPathSelectElement("/retweeted_status/user/id_str").Value;
3193 // 自分に関係しないリツイートの場合は無視する
3194 var selfUserId = this.UserId.ToString();
3195 if (sourceUserId == selfUserId || targetUserId == selfUserId)
3197 // 公式 RT をイベントとしても扱う
3198 var evt = CreateEventFromRetweet(xElm);
3201 this.StoredEvent.Insert(0, evt);
3203 this.UserStreamEventReceived?.Invoke(this, new UserStreamEventReceivedEventArgs(evt));
3207 // 従来通り公式 RT の表示も行うため return しない
3209 else if (xElm.Element("scrub_geo") != null)
3213 TabInformations.GetInstance().ScrubGeoReserve(long.Parse(xElm.Element("scrub_geo").Element("user_id").Value),
3214 long.Parse(xElm.Element("scrub_geo").Element("up_to_status_id").Value));
3218 MyCommon.TraceOut("scrub_geo:" + line);
3226 CreateDirectMessagesFromJson(line, MyCommon.WORKERTYPE.UserStream, false);
3230 CreatePostsFromJson("[" + line + "]", MyCommon.WORKERTYPE.Timeline, null, false);
3233 catch (WebApiException ex)
3235 MyCommon.TraceOut(ex);
3238 catch(NullReferenceException)
3240 MyCommon.TraceOut("NullRef StatusArrived: " + line);
3243 this.NewPostFromStream?.Invoke(this, EventArgs.Empty);
3247 /// UserStreamsから受信した公式RTをイベントに変換します
3249 private FormattedEvent CreateEventFromRetweet(XElement xElm)
3251 return new FormattedEvent
3253 Eventtype = MyCommon.EVENTTYPE.Retweet,
3255 CreatedAt = MyCommon.DateTimeParse(xElm.XPathSelectElement("/created_at").Value),
3256 IsMe = xElm.XPathSelectElement("/user/id_str").Value == this.UserId.ToString(),
3257 Username = xElm.XPathSelectElement("/user/screen_name").Value,
3258 Target = string.Format("@{0}:{1}", new[]
3260 xElm.XPathSelectElement("/retweeted_status/user/screen_name").Value,
3261 WebUtility.HtmlDecode(xElm.XPathSelectElement("/retweeted_status/text").Value),
3263 Id = long.Parse(xElm.XPathSelectElement("/retweeted_status/id_str").Value),
3267 private void CreateEventFromJson(string content)
3269 TwitterStreamEvent eventData = null;
3272 eventData = TwitterStreamEvent.ParseJson(content);
3274 catch(SerializationException ex)
3276 MyCommon.TraceOut(ex, "Event Serialize Exception!" + Environment.NewLine + content);
3280 MyCommon.TraceOut(ex, "Event Exception!" + Environment.NewLine + content);
3283 var evt = new FormattedEvent();
3284 evt.CreatedAt = MyCommon.DateTimeParse(eventData.CreatedAt);
3285 evt.Event = eventData.Event;
3286 evt.Username = eventData.Source.ScreenName;
3287 evt.IsMe = evt.Username.ToLowerInvariant().Equals(this.Username.ToLowerInvariant());
3289 MyCommon.EVENTTYPE eventType;
3290 eventTable.TryGetValue(eventData.Event, out eventType);
3291 evt.Eventtype = eventType;
3293 TwitterStreamEvent<TwitterStatus> tweetEvent;
3295 switch (eventData.Event)
3297 case "access_revoked":
3298 case "access_unrevoked":
3300 case "user_suspend":
3303 if (eventData.Target.ScreenName.ToLowerInvariant().Equals(_uname))
3305 if (!this.followerId.Contains(eventData.Source.Id)) this.followerId.Add(eventData.Source.Id);
3309 return; //Block後のUndoをすると、SourceとTargetが逆転したfollowイベントが帰ってくるため。
3314 evt.Target = "@" + eventData.Target.ScreenName;
3316 case "favorited_retweet":
3317 case "retweeted_retweet":
3321 tweetEvent = TwitterStreamEvent<TwitterStatus>.ParseJson(content);
3322 evt.Target = "@" + tweetEvent.TargetObject.User.ScreenName + ":" + WebUtility.HtmlDecode(tweetEvent.TargetObject.Text);
3323 evt.Id = tweetEvent.TargetObject.Id;
3325 if (SettingCommon.Instance.IsRemoveSameEvent)
3327 if (this.StoredEvent.Any(ev => ev.Username == evt.Username && ev.Eventtype == evt.Eventtype && ev.Target == evt.Target))
3331 var tabinfo = TabInformations.GetInstance();
3334 var statusId = tweetEvent.TargetObject.Id;
3335 if (!tabinfo.Posts.TryGetValue(statusId, out post))
3338 if (eventData.Event == "favorite")
3340 var favTab = tabinfo.GetTabByType(MyCommon.TabUsageType.Favorites);
3341 if (!favTab.Contains(post.StatusId))
3342 favTab.AddPostImmediately(post.StatusId, post.IsRead);
3344 if (tweetEvent.Source.Id == this.UserId)
3348 else if (tweetEvent.Target.Id == this.UserId)
3350 post.FavoritedCount++;
3352 if (SettingCommon.Instance.FavEventUnread)
3353 tabinfo.SetReadAllTab(post.StatusId, read: false);
3358 if (tweetEvent.Source.Id == this.UserId)
3362 else if (tweetEvent.Target.Id == this.UserId)
3364 post.FavoritedCount = Math.Max(0, post.FavoritedCount - 1);
3368 case "quoted_tweet":
3369 if (evt.IsMe) return;
3371 tweetEvent = TwitterStreamEvent<TwitterStatus>.ParseJson(content);
3372 evt.Target = "@" + tweetEvent.TargetObject.User.ScreenName + ":" + WebUtility.HtmlDecode(tweetEvent.TargetObject.Text);
3373 evt.Id = tweetEvent.TargetObject.Id;
3375 if (SettingCommon.Instance.IsRemoveSameEvent)
3377 if (this.StoredEvent.Any(ev => ev.Username == evt.Username && ev.Eventtype == evt.Eventtype && ev.Target == evt.Target))
3381 case "list_member_added":
3382 case "list_member_removed":
3383 case "list_created":
3384 case "list_destroyed":
3385 case "list_updated":
3386 case "list_user_subscribed":
3387 case "list_user_unsubscribed":
3388 var listEvent = TwitterStreamEvent<TwitterList>.ParseJson(content);
3389 evt.Target = listEvent.TargetObject.FullName;
3392 if (!TabInformations.GetInstance().BlockIds.Contains(eventData.Target.Id)) TabInformations.GetInstance().BlockIds.Add(eventData.Target.Id);
3396 if (TabInformations.GetInstance().BlockIds.Contains(eventData.Target.Id)) TabInformations.GetInstance().BlockIds.Remove(eventData.Target.Id);
3405 evt.Target = "@" + eventData.Target.ScreenName;
3406 if (!TabInformations.GetInstance().MuteUserIds.Contains(eventData.Target.Id))
3408 TabInformations.GetInstance().MuteUserIds.Add(eventData.Target.Id);
3412 evt.Target = "@" + eventData.Target.ScreenName;
3413 if (TabInformations.GetInstance().MuteUserIds.Contains(eventData.Target.Id))
3415 TabInformations.GetInstance().MuteUserIds.Remove(eventData.Target.Id);
3420 MyCommon.TraceOut("Unknown Event:" + evt.Event + Environment.NewLine + content);
3423 this.StoredEvent.Insert(0, evt);
3425 this.UserStreamEventReceived?.Invoke(this, new UserStreamEventReceivedEventArgs(evt));
3428 private void userStream_Started()
3430 this.UserStreamStarted?.Invoke(this, EventArgs.Empty);
3433 private void userStream_Stopped()
3435 this.UserStreamStopped?.Invoke(this, EventArgs.Empty);
3438 public bool UserStreamEnabled
3442 return userStream == null ? false : userStream.Enabled;
3446 public void StartUserStream()
3448 if (userStream != null)
3452 userStream = new TwitterUserstream(twCon);
3453 userStream.StatusArrived += userStream_StatusArrived;
3454 userStream.Started += userStream_Started;
3455 userStream.Stopped += userStream_Stopped;
3456 userStream.Start(this.AllAtReply, this.TrackWord);
3459 public void StopUserStream()
3461 userStream?.Dispose();
3463 if (!MyCommon._endingFlag)
3465 this.UserStreamStopped?.Invoke(this, EventArgs.Empty);
3469 public void ReconnectUserStream()
3471 if (userStream != null)
3473 this.StartUserStream();
3477 private class TwitterUserstream : IDisposable
3479 public event Action<string> StatusArrived;
3480 public event Action Stopped;
3481 public event Action Started;
3482 private HttpTwitter twCon;
3484 private Thread _streamThread;
3485 private bool _streamActive;
3487 private bool _allAtreplies = false;
3488 private string _trackwords = "";
3490 public TwitterUserstream(HttpTwitter twitterConnection)
3492 twCon = (HttpTwitter)twitterConnection.Clone();
3495 public void Start(bool allAtReplies, string trackwords)
3497 this.AllAtReplies = allAtReplies;
3498 this.TrackWords = trackwords;
3499 _streamActive = true;
3500 if (_streamThread != null && _streamThread.IsAlive) return;
3501 _streamThread = new Thread(UserStreamLoop);
3502 _streamThread.Name = "UserStreamReceiver";
3503 _streamThread.IsBackground = true;
3504 _streamThread.Start();
3511 return _streamActive;
3515 public bool AllAtReplies
3519 return _allAtreplies;
3523 _allAtreplies = value;
3527 public string TrackWords
3535 _trackwords = value;
3539 private void UserStreamLoop()
3545 StreamReader sr = null;
3548 if (!MyCommon.IsNetworkAvailable())
3556 var res = twCon.UserStream(ref st, _allAtreplies, _trackwords, Networking.GetUserAgentString());
3560 case HttpStatusCode.OK:
3561 Twitter.AccountState = MyCommon.ACCOUNT_STATE.Valid;
3563 case HttpStatusCode.Unauthorized:
3564 Twitter.AccountState = MyCommon.ACCOUNT_STATE.Invalid;
3572 //MyCommon.TraceOut("Stop:stream is null")
3576 sr = new StreamReader(st);
3578 while (_streamActive && !sr.EndOfStream && Twitter.AccountState == MyCommon.ACCOUNT_STATE.Valid)
3580 StatusArrived?.Invoke(sr.ReadLine());
3581 //this.LastTime = Now;
3584 if (sr.EndOfStream || Twitter.AccountState == MyCommon.ACCOUNT_STATE.Invalid)
3587 //MyCommon.TraceOut("Stop:EndOfStream")
3592 catch(WebException ex)
3594 if (ex.Status == WebExceptionStatus.Timeout)
3596 sleepSec = 30; //MyCommon.TraceOut("Stop:Timeout")
3598 else if (ex.Response != null && (int)((HttpWebResponse)ex.Response).StatusCode == 420)
3600 //MyCommon.TraceOut("Stop:Connection Limit")
3606 //MyCommon.TraceOut("Stop:WebException " + ex.Status.ToString())
3609 catch(ThreadAbortException)
3616 //MyCommon.TraceOut("Stop:IOException with Active." + Environment.NewLine + ex.Message)
3618 catch(ArgumentException ex)
3620 //System.ArgumentException: ストリームを読み取れませんでした。
3621 //サーバー側もしくは通信経路上で切断された場合?タイムアウト頻発後発生
3623 MyCommon.TraceOut(ex, "Stop:ArgumentException");
3627 MyCommon.TraceOut("Stop:Exception." + Environment.NewLine + ex.Message);
3628 MyCommon.ExceptionOut(ex);
3637 twCon.RequestAbort();
3642 while (_streamActive && ms < sleepSec * 1000)
3650 } while (this._streamActive);
3656 MyCommon.TraceOut("Stop:EndLoop");
3659 #region "IDisposable Support"
3660 private bool disposedValue; // 重複する呼び出しを検出するには
3663 protected virtual void Dispose(bool disposing)
3665 if (!this.disposedValue)
3669 _streamActive = false;
3670 if (_streamThread != null && _streamThread.IsAlive)
3672 _streamThread.Abort();
3676 this.disposedValue = true;
3679 //protected Overrides void Finalize()
3681 // // このコードを変更しないでください。クリーンアップ コードを上の Dispose(bool disposing) に記述します。
3683 // MyBase.Finalize()
3686 // このコードは、破棄可能なパターンを正しく実装できるように Visual Basic によって追加されました。
3687 public void Dispose()
3689 // このコードを変更しないでください。クリーンアップ コードを上の Dispose(bool disposing) に記述します。
3691 GC.SuppressFinalize(this);
3698 #region "IDisposable Support"
3699 private bool disposedValue; // 重複する呼び出しを検出するには
3702 protected virtual void Dispose(bool disposing)
3704 if (!this.disposedValue)
3708 this.StopUserStream();
3711 this.disposedValue = true;
3714 //protected Overrides void Finalize()
3716 // // このコードを変更しないでください。クリーンアップ コードを上の Dispose(bool disposing) に記述します。
3718 // MyBase.Finalize()
3721 // このコードは、破棄可能なパターンを正しく実装できるように Visual Basic によって追加されました。
3722 public void Dispose()
3724 // このコードを変更しないでください。クリーンアップ コードを上の Dispose(bool disposing) に記述します。
3726 GC.SuppressFinalize(this);
3731 public class PostDeletedEventArgs : EventArgs
3733 public long StatusId { get; }
3735 public PostDeletedEventArgs(long statusId)
3737 this.StatusId = statusId;
3741 public class UserStreamEventReceivedEventArgs : EventArgs
3743 public Twitter.FormattedEvent EventData { get; }
3745 public UserStreamEventReceivedEventArgs(Twitter.FormattedEvent eventData)
3747 this.EventData = eventData;