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.Api.DataModel;
50 using OpenTween.Connection;
54 public class Twitter : IDisposable
56 #region Regexp from twitter-text-js
58 // The code in this region code block incorporates works covered by
59 // the following copyright and permission notices:
61 // Copyright 2011 Twitter, Inc.
63 // Licensed under the Apache License, Version 2.0 (the "License"); you
64 // may not use this work except in compliance with the License. You
65 // may obtain a copy of the License in the LICENSE file, or at:
67 // http://www.apache.org/licenses/LICENSE-2.0
69 // Unless required by applicable law or agreed to in writing, software
70 // distributed under the License is distributed on an "AS IS" BASIS,
71 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
72 // implied. See the License for the specific language governing
73 // permissions and limitations under the License.
76 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";
77 private const string NON_LATIN_HASHTAG_CHARS = @"\u0400-\u04ff\u0500-\u0527\u1100-\u11ff\u3130-\u3185\uA960-\uA97F\uAC00-\uD7AF\uD7B0-\uD7FF";
78 //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";
79 private const string CJ_HASHTAG_CHARACTERS = @"\u30A1-\u30FA\u30FC\u3005\uFF66-\uFF9F\uFF10-\uFF19\uFF21-\uFF3A\uFF41-\uFF5A\u3041-\u309A\u3400-\u4DBF\p{IsCJKUnifiedIdeographs}";
80 private const string HASHTAG_BOUNDARY = @"^|$|\s|「|」|。|\.|!";
81 private const string HASHTAG_ALPHA = "[a-z_" + LATIN_ACCENTS + NON_LATIN_HASHTAG_CHARS + CJ_HASHTAG_CHARACTERS + "]";
82 private const string HASHTAG_ALPHANUMERIC = "[a-z0-9_" + LATIN_ACCENTS + NON_LATIN_HASHTAG_CHARS + CJ_HASHTAG_CHARACTERS + "]";
83 private const string HASHTAG_TERMINATOR = "[^a-z0-9_" + LATIN_ACCENTS + NON_LATIN_HASHTAG_CHARS + CJ_HASHTAG_CHARACTERS + "]";
84 public const string HASHTAG = "(" + HASHTAG_BOUNDARY + ")(#|#)(" + HASHTAG_ALPHANUMERIC + "*" + HASHTAG_ALPHA + HASHTAG_ALPHANUMERIC + "*)(?=" + HASHTAG_TERMINATOR + "|" + HASHTAG_BOUNDARY + ")";
86 private const string url_valid_preceding_chars = @"(?:[^A-Za-z0-9@@$##\ufffe\ufeff\uffff\u202a-\u202e]|^)";
87 public const string url_invalid_without_protocol_preceding_chars = @"[-_./]$";
88 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";
89 private const string url_valid_domain_chars = @"[^" + url_invalid_domain_chars + "]";
90 private const string url_valid_subdomain = @"(?:(?:" + url_valid_domain_chars + @"(?:[_-]|" + url_valid_domain_chars + @")*)?" + url_valid_domain_chars + @"\.)";
91 private const string url_valid_domain_name = @"(?:(?:" + url_valid_domain_chars + @"(?:-|" + url_valid_domain_chars + @")*)?" + url_valid_domain_chars + @"\.)";
92 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]|$))";
93 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]|$))";
94 private const string url_valid_punycode = @"(?:xn--[0-9a-z]+)";
95 private const string url_valid_domain = @"(?<domain>" + url_valid_subdomain + "*" + url_valid_domain_name + "(?:" + url_valid_GTLD + "|" + url_valid_CCTLD + ")|" + url_valid_punycode + ")";
96 public const string url_valid_ascii_domain = @"(?:(?:[a-z0-9" + LATIN_ACCENTS + @"]+)\.)+(?:" + url_valid_GTLD + "|" + url_valid_CCTLD + "|" + url_valid_punycode + ")";
97 public const string url_invalid_short_domain = "^" + url_valid_domain_name + url_valid_CCTLD + "$";
98 private const string url_valid_port_number = @"[0-9]+";
100 private const string url_valid_general_path_chars = @"[a-z0-9!*';:=+,.$/%#\[\]\-_~|&" + LATIN_ACCENTS + "]";
101 private const string url_balance_parens = @"(?:\(" + url_valid_general_path_chars + @"+\))";
102 private const string url_valid_path_ending_chars = @"(?:[+\-a-z0-9=_#/" + LATIN_ACCENTS + "]|" + url_balance_parens + ")";
103 private const string pth = "(?:" +
105 url_valid_general_path_chars + "*" +
106 "(?:" + url_balance_parens + url_valid_general_path_chars + "*)*" +
107 url_valid_path_ending_chars +
108 ")|(?:@" + url_valid_general_path_chars + "+/)" +
110 private const string qry = @"(?<query>\?[a-z0-9!?*'();:&=+$/%#\[\]\-_.,~|]*[a-z0-9_&=#/])?";
111 public const string rgUrl = @"(?<before>" + url_valid_preceding_chars + ")" +
112 "(?<url>(?<protocol>https?://)?" +
113 "(?<domain>" + url_valid_domain + ")" +
114 "(?::" + url_valid_port_number + ")?" +
115 "(?<path>/" + pth + "*)?" +
122 /// Twitter API のステータスページのURL
124 public const string ServiceAvailabilityStatusUrl = "https://status.io.watchmouse.com/7617";
127 /// ツイートへのパーマリンクURLを判定する正規表現
129 public static readonly Regex StatusUrlRegex = new Regex(@"https?://([^.]+\.)?twitter\.com/(#!/)?(?<ScreenName>[a-zA-Z0-9_]+)/status(es)?/(?<StatusId>[0-9]+)(/photo)?", RegexOptions.IgnoreCase);
132 /// FavstarやaclogなどTwitter関連サービスのパーマリンクURLからステータスIDを抽出する正規表現
134 public static readonly Regex ThirdPartyStatusUrlRegex = new Regex(@"https?://(?:[^.]+\.)?(?:
135 favstar\.fm/users/[a-zA-Z0-9_]+/status/ # Favstar
136 | favstar\.fm/t/ # Favstar (short)
137 | aclog\.koba789\.com/i/ # aclog
138 | frtrt\.net/solo_status\.php\?status= # RtRT
139 )(?<StatusId>[0-9]+)", RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace);
142 /// DM送信かどうかを判定する正規表現
144 public static readonly Regex DMSendTextRegex = new Regex(@"^DM? +(?<id>[a-zA-Z0-9_]+) +(?<body>.*)", RegexOptions.IgnoreCase | RegexOptions.Singleline);
146 public TwitterConfiguration Configuration { get; private set; }
148 delegate void GetIconImageDelegate(PostClass post);
149 private readonly object LockObj = new object();
150 private ISet<long> followerId = new HashSet<long>();
151 private bool _GetFollowerResult = false;
152 private long[] noRTId = new long[0];
153 private bool _GetNoRetweetResult = false;
156 private string _uname;
158 private bool _readOwnPost;
159 private List<string> _hashList = new List<string>();
161 //max_idで古い発言を取得するために保持(lists分は個別タブで管理)
162 private long minHomeTimeline = long.MaxValue;
163 private long minMentions = long.MaxValue;
164 private long minDirectmessage = long.MaxValue;
165 private long minDirectmessageSent = long.MaxValue;
167 //private FavoriteQueue favQueue;
169 private HttpTwitter twCon = new HttpTwitter();
171 //private List<PostClass> _deletemessages = new List<PostClass>();
175 this.Configuration = TwitterConfiguration.DefaultConfiguration();
178 public TwitterApiAccessLevel AccessLevel
182 return MyCommon.TwitterApiInfo.AccessLevel;
186 protected void ResetApiStatus()
188 MyCommon.TwitterApiInfo.Reset();
191 public void Authenticate(string username, string password)
193 this.ResetApiStatus();
199 res = twCon.AuthUserAndPass(username, password, ref content);
203 throw new WebApiException("Err:" + ex.Message, ex);
206 this.CheckStatusCode(res, content);
208 _uname = username.ToLowerInvariant();
209 if (SettingCommon.Instance.UserstreamStartup) this.ReconnectUserStream();
212 public string StartAuthentication()
215 this.ResetApiStatus();
218 string pinPageUrl = null;
219 var res = twCon.AuthGetRequestToken(ref pinPageUrl);
221 throw new WebApiException("Err:Failed to access auth server.");
227 throw new WebApiException("Err:Failed to access auth server.", ex);
231 public void Authenticate(string pinCode)
233 this.ResetApiStatus();
238 res = twCon.AuthGetAccessToken(pinCode);
242 throw new WebApiException("Err:Failed to access auth acc server.", ex);
245 this.CheckStatusCode(res, null);
247 _uname = Username.ToLowerInvariant();
248 if (SettingCommon.Instance.UserstreamStartup) this.ReconnectUserStream();
251 public void ClearAuthInfo()
253 Twitter.AccountState = MyCommon.ACCOUNT_STATE.Invalid;
254 this.ResetApiStatus();
255 twCon.ClearAuthInfo();
258 public void VerifyCredentials()
264 res = twCon.VerifyCredentials(ref content);
268 throw new WebApiException("Err:" + ex.Message, ex);
271 this.CheckStatusCode(res, content);
275 var user = TwitterUser.ParseJson(content);
277 this.twCon.AuthenticatedUserId = user.Id;
278 this.UpdateUserStats(user);
280 catch (SerializationException ex)
282 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
283 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
287 public void Initialize(string token, string tokenSecret, string username, long userId)
290 if (string.IsNullOrEmpty(token) || string.IsNullOrEmpty(tokenSecret) || string.IsNullOrEmpty(username))
292 Twitter.AccountState = MyCommon.ACCOUNT_STATE.Invalid;
294 this.ResetApiStatus();
295 twCon.Initialize(token, tokenSecret, username, userId);
296 _uname = username.ToLowerInvariant();
297 if (SettingCommon.Instance.UserstreamStartup) this.ReconnectUserStream();
300 public string PreProcessUrl(string orgData)
304 //var IDNConveter = new IdnMapping();
305 var href = "<a href=\"";
309 if (orgData.IndexOf(href, posl2, StringComparison.Ordinal) > -1)
313 posl1 = orgData.IndexOf(href, posl2, StringComparison.Ordinal);
314 posl1 += href.Length;
315 posl2 = orgData.IndexOf("\"", posl1, StringComparison.Ordinal);
316 urlStr = orgData.Substring(posl1, posl2 - posl1);
318 if (!urlStr.StartsWith("http://", StringComparison.Ordinal)
319 && !urlStr.StartsWith("https://", StringComparison.Ordinal)
320 && !urlStr.StartsWith("ftp://", StringComparison.Ordinal))
325 var replacedUrl = MyCommon.IDNEncode(urlStr);
326 if (replacedUrl == null) continue;
327 if (replacedUrl == urlStr) continue;
329 orgData = orgData.Replace("<a href=\"" + urlStr, "<a href=\"" + replacedUrl);
340 private string GetPlainText(string orgData)
342 return WebUtility.HtmlDecode(Regex.Replace(orgData, "(?<tagStart><a [^>]+>)(?<text>[^<]+)(?<tagEnd></a>)", "${text}"));
345 // htmlの簡易サニタイズ(詳細表示に不要なタグの除去)
347 private string SanitizeHtml(string orgdata)
349 var retdata = orgdata;
351 retdata = Regex.Replace(retdata, "<(script|object|applet|image|frameset|fieldset|legend|style).*" +
352 "</(script|object|applet|image|frameset|fieldset|legend|style)>", "", RegexOptions.IgnoreCase);
354 retdata = Regex.Replace(retdata, "<(frame|link|iframe|img)>", "", RegexOptions.IgnoreCase);
359 private string AdjustHtml(string orgData)
361 var retStr = orgData;
362 //var m = Regex.Match(retStr, "<a [^>]+>[#|#](?<1>[a-zA-Z0-9_]+)</a>");
367 // _hashList.Add("#" + m.Groups(1).Value);
371 retStr = Regex.Replace(retStr, "<a [^>]*href=\"/", "<a href=\"https://twitter.com/");
372 retStr = retStr.Replace("<a href=", "<a target=\"_self\" href=");
373 retStr = Regex.Replace(retStr, @"(\r\n?|\n)", "<br>"); // CRLF, CR, LF は全て <br> に置換する
375 //半角スペースを置換(Thanks @anis774)
379 ret = EscapeSpace(ref retStr);
382 return SanitizeHtml(retStr);
385 private bool EscapeSpace(ref string html)
387 //半角スペースを置換(Thanks @anis774)
389 for (int i = 0; i < html.Length; i++)
400 if ((!isTag) && (html[i] == ' '))
402 html = html.Remove(i, 1);
403 html = html.Insert(i, " ");
410 private struct PostInfo
412 public string CreatedAt;
415 public string UserId;
416 public PostInfo(string Created, string IdStr, string txt, string uid)
423 public bool Equals(PostInfo dst)
425 if (this.CreatedAt == dst.CreatedAt && this.Id == dst.Id && this.Text == dst.Text && this.UserId == dst.UserId)
436 static private PostInfo _prev = new PostInfo("", "", "", "");
437 private bool IsPostRestricted(TwitterStatus status)
439 var _current = new PostInfo("", "", "", "");
441 _current.CreatedAt = status.CreatedAt;
442 _current.Id = status.IdStr;
443 if (status.Text == null)
449 _current.Text = status.Text;
451 _current.UserId = status.User.IdStr;
453 if (_current.Equals(_prev))
457 _prev.CreatedAt = _current.CreatedAt;
458 _prev.Id = _current.Id;
459 _prev.Text = _current.Text;
460 _prev.UserId = _current.UserId;
465 public void PostStatus(string postStr, long? reply_to, List<long> mediaIds = null)
467 this.CheckAccountState();
469 if (mediaIds == null &&
470 Twitter.DMSendTextRegex.IsMatch(postStr))
472 SendDirectMessage(postStr);
480 res = twCon.UpdateStatus(postStr, reply_to, mediaIds, ref content);
484 throw new WebApiException("Err:" + ex.Message, ex);
487 // 投稿に成功していても404が返ることがあるらしい: https://dev.twitter.com/discussions/1213
488 if (res == HttpStatusCode.NotFound)
491 this.CheckStatusCode(res, content);
493 TwitterStatus status;
496 status = TwitterStatus.ParseJson(content);
498 catch(SerializationException ex)
500 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
501 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
505 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
506 throw new WebApiException("Err:Invalid Json!", content, ex);
509 this.UpdateUserStats(status.User);
511 if (IsPostRestricted(status))
513 throw new WebApiException("OK:Delaying?");
517 public void PostStatusWithMedia(string postStr, long? reply_to, IMediaItem item)
519 this.CheckAccountState();
525 res = twCon.UpdateStatusWithMedia(postStr, reply_to, item, ref content);
529 throw new WebApiException("Err:" + ex.Message, ex);
532 // 投稿に成功していても404が返ることがあるらしい: https://dev.twitter.com/discussions/1213
533 if (res == HttpStatusCode.NotFound)
536 this.CheckStatusCode(res, content);
538 TwitterStatus status;
541 status = TwitterStatus.ParseJson(content);
543 catch(SerializationException ex)
545 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
546 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
550 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
551 throw new WebApiException("Err:Invalid Json!", content, ex);
554 this.UpdateUserStats(status.User);
556 if (IsPostRestricted(status))
558 throw new WebApiException("OK:Delaying?");
562 public void PostStatusWithMultipleMedia(string postStr, long? reply_to, IMediaItem[] mediaItems)
564 this.CheckAccountState();
566 if (Twitter.DMSendTextRegex.IsMatch(postStr))
568 SendDirectMessage(postStr);
572 var mediaIds = new List<long>();
574 foreach (var item in mediaItems)
576 var mediaId = UploadMedia(item);
577 mediaIds.Add(mediaId);
580 if (mediaIds.Count == 0)
581 throw new WebApiException("Err:Invalid Files!");
583 PostStatus(postStr, reply_to, mediaIds);
586 public long UploadMedia(IMediaItem item)
588 this.CheckAccountState();
594 res = twCon.UploadMedia(item, ref content);
598 throw new WebApiException("Err:" + ex.Message, ex);
601 this.CheckStatusCode(res, content);
603 TwitterUploadMediaResult status;
606 status = TwitterUploadMediaResult.ParseJson(content);
608 catch (SerializationException ex)
610 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
611 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
615 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
616 throw new WebApiException("Err:Invalid Json!", content, ex);
619 return status.MediaId;
622 public void SendDirectMessage(string postStr)
624 this.CheckAccountState();
625 this.CheckAccessLevel(TwitterApiAccessLevel.ReadWriteAndDirectMessage);
627 var mc = Twitter.DMSendTextRegex.Match(postStr);
633 res = twCon.SendDirectMessage(mc.Groups["body"].Value, mc.Groups["id"].Value, ref content);
637 throw new WebApiException("Err:" + ex.Message, ex);
640 this.CheckStatusCode(res, content);
642 TwitterDirectMessage status;
645 status = TwitterDirectMessage.ParseJson(content);
647 catch(SerializationException ex)
649 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
650 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
654 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
655 throw new WebApiException("Err:Invalid Json!", content, ex);
658 this.UpdateUserStats(status.Sender);
661 public void RemoveStatus(long id)
663 this.CheckAccountState();
668 res = twCon.DestroyStatus(id);
672 throw new WebApiException("Err:" + ex.Message, ex);
675 this.CheckStatusCode(res, null);
678 public void PostRetweet(long id, bool read)
680 this.CheckAccountState();
684 var post = TabInformations.GetInstance()[id];
687 throw new WebApiException("Err:Target isn't found.");
689 if (TabInformations.GetInstance()[id].RetweetedId != null)
691 target = TabInformations.GetInstance()[id].RetweetedId.Value; //再RTの場合は元発言をRT
698 res = twCon.RetweetStatus(target, ref content);
702 throw new WebApiException("Err:" + ex.Message, ex);
705 this.CheckStatusCode(res, content);
707 TwitterStatus status;
710 status = TwitterStatus.ParseJson(content);
712 catch(SerializationException ex)
714 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
715 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
719 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
720 throw new WebApiException("Err:Invalid Json!", content, ex);
724 post = CreatePostsFromStatusData(status);
726 throw new WebApiException("Invalid Json!", content);
731 if (TabInformations.GetInstance().ContainsKey(post.StatusId))
735 if (post.RetweetedId == null)
736 throw new WebApiException("Invalid Json!", content);
742 if (_readOwnPost) post.IsRead = true;
745 TabInformations.GetInstance().AddPost(post);
748 public void RemoveDirectMessage(long id, PostClass post)
750 this.CheckAccountState();
751 this.CheckAccessLevel(TwitterApiAccessLevel.ReadWriteAndDirectMessage);
754 // _deletemessages.Add(post)
760 res = twCon.DestroyDirectMessage(id);
764 throw new WebApiException("Err:" + ex.Message, ex);
767 this.CheckStatusCode(res, null);
770 public void PostFollowCommand(string screenName)
772 this.CheckAccountState();
778 res = twCon.CreateFriendships(screenName, ref content);
782 throw new WebApiException("Err:" + ex.Message, ex);
785 this.CheckStatusCode(res, content);
788 public void PostRemoveCommand(string screenName)
790 this.CheckAccountState();
796 res = twCon.DestroyFriendships(screenName, ref content);
800 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
803 this.CheckStatusCode(res, content);
806 public void PostCreateBlock(string screenName)
808 this.CheckAccountState();
814 res = twCon.CreateBlock(screenName, ref content);
818 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
821 this.CheckStatusCode(res, content);
824 public void PostDestroyBlock(string screenName)
826 this.CheckAccountState();
832 res = twCon.DestroyBlock(screenName, ref content);
836 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
839 this.CheckStatusCode(res, content);
842 public void PostReportSpam(string screenName)
844 this.CheckAccountState();
850 res = twCon.ReportSpam(screenName, ref content);
854 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
857 this.CheckStatusCode(res, content);
860 public TwitterFriendship GetFriendshipInfo(string screenName)
862 this.CheckAccountState();
868 res = twCon.ShowFriendships(_uname, screenName, ref content);
872 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
875 this.CheckStatusCode(res, content);
879 return TwitterFriendship.ParseJson(content);
881 catch(SerializationException ex)
883 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
884 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
888 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
889 throw new WebApiException("Err:Invalid Json!", content, ex);
893 public TwitterUser GetUserInfo(string screenName)
895 this.CheckAccountState();
901 res = twCon.ShowUserInfo(screenName, ref content);
905 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
908 this.CheckStatusCode(res, content);
912 return TwitterUser.ParseJson(content);
914 catch (SerializationException ex)
916 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
917 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
921 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
922 throw new WebApiException("Err:Invalid Json!", content, ex);
926 public int GetStatus_Retweeted_Count(long StatusId)
928 this.CheckAccountState();
934 res = twCon.ShowStatuses(StatusId, ref content);
938 throw new WebApiException("Err:" + ex.Message, ex);
941 this.CheckStatusCode(res, content);
945 var status = TwitterStatus.ParseJson(content);
946 return status.RetweetCount;
948 catch (SerializationException ex)
950 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
951 throw new WebApiException("Json Parse Error(DataContractJsonSerializer)", content, ex);
955 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
956 throw new WebApiException("Invalid Json!", content, ex);
960 public void PostFavAdd(long id)
962 this.CheckAccountState();
964 //if (this.favQueue == null) this.favQueue = new FavoriteQueue(this)
966 //if (this.favQueue.Contains(id)) this.favQueue.Remove(id)
972 res = twCon.CreateFavorites(id, ref content);
976 //this.favQueue.Add(id)
977 //return "Err:->FavoriteQueue:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")";
978 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
981 this.CheckStatusCode(res, content);
983 if (!RestrictFavCheck)
986 //http://twitter.com/statuses/show/id.xml APIを発行して本文を取得
990 res = twCon.ShowStatuses(id, ref content);
994 throw new WebApiException("Err:" + ex.Message, ex);
997 this.CheckStatusCode(res, content);
999 TwitterStatus status;
1002 status = TwitterStatus.ParseJson(content);
1004 catch (SerializationException ex)
1006 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
1007 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
1009 catch (Exception ex)
1011 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
1012 throw new WebApiException("Err:Invalid Json!", content, ex);
1014 if (status.Favorited != true)
1015 throw new WebApiException("NG(Restricted?)");
1018 public void PostFavRemove(long id)
1020 this.CheckAccountState();
1022 //if (this.favQueue == null) this.favQueue = new FavoriteQueue(this)
1024 //if (this.favQueue.Contains(id))
1025 // this.favQueue.Remove(id)
1033 res = twCon.DestroyFavorites(id, ref content);
1037 throw new WebApiException("Err:" + ex.Message, ex);
1040 this.CheckStatusCode(res, content);
1043 public TwitterUser PostUpdateProfile(string name, string url, string location, string description)
1045 this.CheckAccountState();
1051 res = twCon.UpdateProfile(name, url, location, description, ref content);
1055 throw new WebApiException("Err:" + ex.Message, content, ex);
1058 this.CheckStatusCode(res, content);
1062 return TwitterUser.ParseJson(content);
1064 catch (SerializationException e)
1066 var ex = new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, e);
1067 MyCommon.TraceOut(ex);
1072 var ex = new WebApiException("Err:Invalid Json!", content, e);
1073 MyCommon.TraceOut(ex);
1078 public void PostUpdateProfileImage(string filename)
1080 this.CheckAccountState();
1086 res = twCon.UpdateProfileImage(new FileInfo(filename), ref content);
1090 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
1093 this.CheckStatusCode(res, content);
1096 public string Username
1100 return twCon.AuthenticatedUsername;
1108 return twCon.AuthenticatedUserId;
1112 public string Password
1116 return twCon.Password;
1120 private static MyCommon.ACCOUNT_STATE _accountState = MyCommon.ACCOUNT_STATE.Valid;
1121 public static MyCommon.ACCOUNT_STATE AccountState
1125 return _accountState;
1129 _accountState = value;
1133 public bool RestrictFavCheck { get; set; }
1136 public void GetTweenBinary(string strVer)
1141 if (!(new HttpVarious()).GetDataToFile("http://tween.sourceforge.jp/Tween" + strVer + ".gz?" + DateTime.Now.ToString("yyMMddHHmmss") + Environment.TickCount.ToString(),
1142 Path.Combine(MyCommon.settingPath, "TweenNew.exe")))
1144 throw new WebApiException("Err:Download failed");
1147 if (!Directory.Exists(Path.Combine(MyCommon.settingPath, "en")))
1149 Directory.CreateDirectory(Path.Combine(MyCommon.settingPath, "en"));
1151 if (!(new HttpVarious()).GetDataToFile("http://tween.sourceforge.jp/TweenResEn" + strVer + ".gz?" + DateTime.Now.ToString("yyMMddHHmmss") + Environment.TickCount.ToString(),
1152 Path.Combine(Path.Combine(MyCommon.settingPath, "en"), "Tween.resourcesNew.dll")))
1154 throw new WebApiException("Err:Download failed");
1156 //その他言語圏のリソース。取得失敗しても継続
1159 if (!Thread.CurrentThread.CurrentUICulture.IsNeutralCulture)
1161 var idx = Thread.CurrentThread.CurrentUICulture.Name.LastIndexOf('-');
1164 curCul = Thread.CurrentThread.CurrentUICulture.Name.Substring(0, idx);
1168 curCul = Thread.CurrentThread.CurrentUICulture.Name;
1173 curCul = Thread.CurrentThread.CurrentUICulture.Name;
1175 if (!string.IsNullOrEmpty(curCul) && curCul != "en" && curCul != "ja")
1177 if (!Directory.Exists(Path.Combine(MyCommon.settingPath, curCul)))
1179 Directory.CreateDirectory(Path.Combine(MyCommon.settingPath, curCul));
1181 if (!(new HttpVarious()).GetDataToFile("http://tween.sourceforge.jp/TweenRes" + curCul + strVer + ".gz?" + DateTime.Now.ToString("yyMMddHHmmss") + Environment.TickCount.ToString(),
1182 Path.Combine(Path.Combine(MyCommon.settingPath, curCul), "Tween.resourcesNew.dll")))
1184 //return "Err:Download failed";
1189 if (!Thread.CurrentThread.CurrentCulture.IsNeutralCulture)
1191 var idx = Thread.CurrentThread.CurrentCulture.Name.LastIndexOf('-');
1194 curCul2 = Thread.CurrentThread.CurrentCulture.Name.Substring(0, idx);
1198 curCul2 = Thread.CurrentThread.CurrentCulture.Name;
1203 curCul2 = Thread.CurrentThread.CurrentCulture.Name;
1205 if (!string.IsNullOrEmpty(curCul2) && curCul2 != "en" && curCul2 != curCul)
1207 if (!Directory.Exists(Path.Combine(MyCommon.settingPath, curCul2)))
1209 Directory.CreateDirectory(Path.Combine(MyCommon.settingPath, curCul2));
1211 if (!(new HttpVarious()).GetDataToFile("http://tween.sourceforge.jp/TweenRes" + curCul2 + strVer + ".gz?" + DateTime.Now.ToString("yyMMddHHmmss") + Environment.TickCount.ToString(),
1212 Path.Combine(Path.Combine(MyCommon.settingPath, curCul2), "Tween.resourcesNew.dll")))
1214 //return "Err:Download failed";
1219 if (!(new HttpVarious()).GetDataToFile("http://tween.sourceforge.jp/TweenUp3.gz?" + DateTime.Now.ToString("yyMMddHHmmss") + Environment.TickCount.ToString(),
1220 Path.Combine(MyCommon.settingPath, "TweenUp3.exe")))
1222 throw new WebApiException("Err:Download failed");
1225 if (!(new HttpVarious()).GetDataToFile("http://tween.sourceforge.jp/TweenDll" + strVer + ".gz?" + DateTime.Now.ToString("yyMMddHHmmss") + Environment.TickCount.ToString(),
1226 Path.Combine(MyCommon.settingPath, "TweenNew.XmlSerializers.dll")))
1228 throw new WebApiException("Err:Download failed");
1231 catch (Exception ex)
1233 throw new WebApiException("Err:Download failed", ex);
1238 public bool ReadOwnPost
1242 return _readOwnPost;
1246 _readOwnPost = value;
1250 public int FollowersCount { get; private set; }
1251 public int FriendsCount { get; private set; }
1252 public int StatusesCount { get; private set; }
1253 public string Location { get; private set; } = "";
1254 public string Bio { get; private set; } = "";
1256 /// <summary>ユーザーのフォロワー数などの情報を更新します</summary>
1257 private void UpdateUserStats(TwitterUser self)
1259 this.FollowersCount = self.FollowersCount;
1260 this.FriendsCount = self.FriendsCount;
1261 this.StatusesCount = self.StatusesCount;
1262 this.Location = self.Location;
1263 this.Bio = self.Description;
1267 /// 渡された取得件数がWORKERTYPEに応じた取得可能範囲に収まっているか検証する
1269 public static bool VerifyApiResultCount(MyCommon.WORKERTYPE type, int count)
1271 return count >= 20 && count <= GetMaxApiResultCount(type);
1275 /// 渡された取得件数が更新時の取得可能範囲に収まっているか検証する
1277 public static bool VerifyMoreApiResultCount(int count)
1279 return count >= 20 && count <= 200;
1283 /// 渡された取得件数が起動時の取得可能範囲に収まっているか検証する
1285 public static bool VerifyFirstApiResultCount(int count)
1287 return count >= 20 && count <= 200;
1291 /// WORKERTYPEに応じた取得可能な最大件数を取得する
1293 public static int GetMaxApiResultCount(MyCommon.WORKERTYPE type)
1295 // 参照: REST APIs - 各endpointのcountパラメータ
1296 // https://dev.twitter.com/rest/public
1299 case MyCommon.WORKERTYPE.Timeline:
1300 case MyCommon.WORKERTYPE.Reply:
1301 case MyCommon.WORKERTYPE.UserTimeline:
1302 case MyCommon.WORKERTYPE.Favorites:
1303 case MyCommon.WORKERTYPE.DirectMessegeRcv:
1304 case MyCommon.WORKERTYPE.DirectMessegeSnt:
1305 case MyCommon.WORKERTYPE.List: // 不明
1308 case MyCommon.WORKERTYPE.PublicSearch:
1312 throw new InvalidOperationException("Invalid type: " + type);
1317 /// WORKERTYPEに応じた取得件数を取得する
1319 public static int GetApiResultCount(MyCommon.WORKERTYPE type, bool more, bool startup)
1321 if (type == MyCommon.WORKERTYPE.DirectMessegeRcv ||
1322 type == MyCommon.WORKERTYPE.DirectMessegeSnt)
1327 if (SettingCommon.Instance.UseAdditionalCount)
1331 case MyCommon.WORKERTYPE.Favorites:
1332 if (SettingCommon.Instance.FavoritesCountApi != 0)
1333 return SettingCommon.Instance.FavoritesCountApi;
1335 case MyCommon.WORKERTYPE.List:
1336 if (SettingCommon.Instance.ListCountApi != 0)
1337 return SettingCommon.Instance.ListCountApi;
1339 case MyCommon.WORKERTYPE.PublicSearch:
1340 if (SettingCommon.Instance.SearchCountApi != 0)
1341 return SettingCommon.Instance.SearchCountApi;
1343 case MyCommon.WORKERTYPE.UserTimeline:
1344 if (SettingCommon.Instance.UserTimelineCountApi != 0)
1345 return SettingCommon.Instance.UserTimelineCountApi;
1348 if (more && SettingCommon.Instance.MoreCountApi != 0)
1350 return Math.Min(SettingCommon.Instance.MoreCountApi, GetMaxApiResultCount(type));
1352 if (startup && SettingCommon.Instance.FirstCountApi != 0 && type != MyCommon.WORKERTYPE.Reply)
1354 return Math.Min(SettingCommon.Instance.FirstCountApi, GetMaxApiResultCount(type));
1358 // 上記に当てはまらない場合の共通処理
1359 var count = SettingCommon.Instance.CountApi;
1361 if (type == MyCommon.WORKERTYPE.Reply)
1362 count = SettingCommon.Instance.CountApiReply;
1364 return Math.Min(count, GetMaxApiResultCount(type));
1367 public void GetTimelineApi(bool read,
1368 MyCommon.WORKERTYPE gType,
1372 this.CheckAccountState();
1376 var count = GetApiResultCount(gType, more, startup);
1380 if (gType == MyCommon.WORKERTYPE.Timeline)
1384 res = twCon.HomeTimeline(count, this.minHomeTimeline, null, ref content);
1388 res = twCon.HomeTimeline(count, null, null, ref content);
1395 res = twCon.Mentions(count, this.minMentions, null, ref content);
1399 res = twCon.Mentions(count, null, null, ref content);
1405 throw new WebApiException("Err:" + ex.Message, ex);
1408 this.CheckStatusCode(res, content);
1410 var minimumId = CreatePostsFromJson(content, gType, null, read);
1412 if (minimumId != null)
1414 if (gType == MyCommon.WORKERTYPE.Timeline)
1415 this.minHomeTimeline = minimumId.Value;
1417 this.minMentions = minimumId.Value;
1421 public void GetUserTimelineApi(bool read,
1426 this.CheckAccountState();
1430 var count = GetApiResultCount(MyCommon.WORKERTYPE.UserTimeline, more, false);
1434 if (string.IsNullOrEmpty(userName))
1436 var target = tab.User;
1437 if (string.IsNullOrEmpty(target)) return;
1439 res = twCon.UserTimeline(null, target, count, null, null, ref content);
1445 res = twCon.UserTimeline(null, userName, count, tab.OldestId, null, ref content);
1449 res = twCon.UserTimeline(null, userName, count, null, null, ref content);
1455 throw new WebApiException("Err:" + ex.Message, ex);
1458 if (res == HttpStatusCode.Unauthorized)
1459 throw new WebApiException("Err:@" + userName + "'s Tweets are protected.");
1461 this.CheckStatusCode(res, content);
1463 var minimumId = CreatePostsFromJson(content, MyCommon.WORKERTYPE.UserTimeline, tab, read);
1465 if (minimumId != null)
1466 tab.OldestId = minimumId.Value;
1469 public PostClass GetStatusApi(bool read, long id)
1471 this.CheckAccountState();
1477 res = twCon.ShowStatuses(id, ref content);
1481 throw new WebApiException("Err:" + ex.Message, ex);
1484 if (res == HttpStatusCode.Forbidden)
1485 throw new WebApiException("Err:protected user's tweet", content);
1487 this.CheckStatusCode(res, content);
1489 TwitterStatus status;
1492 status = TwitterStatus.ParseJson(content);
1494 catch(SerializationException ex)
1496 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
1497 throw new WebApiException("Json Parse Error(DataContractJsonSerializer)", content, ex);
1501 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
1502 throw new WebApiException("Invalid Json!", content, ex);
1505 var item = CreatePostsFromStatusData(status);
1507 throw new WebApiException("Err:Can't create post", content);
1510 if (item.IsMe && !read && _readOwnPost) item.IsRead = true;
1515 public void GetStatusApi(bool read, long id, TabClass tab)
1517 var post = this.GetStatusApi(read, id);
1519 //非同期アイコン取得&StatusDictionaryに追加
1520 if (tab != null && tab.IsInnerStorageTabType)
1521 tab.AddPostToInnerStorage(post);
1523 TabInformations.GetInstance().AddPost(post);
1526 private PostClass CreatePostsFromStatusData(TwitterStatus status)
1528 return CreatePostsFromStatusData(status, false);
1531 private PostClass CreatePostsFromStatusData(TwitterStatus status, bool favTweet)
1533 var post = new PostClass();
1534 TwitterEntities entities;
1537 post.StatusId = status.Id;
1538 if (status.RetweetedStatus != null)
1540 var retweeted = status.RetweetedStatus;
1542 post.CreatedAt = MyCommon.DateTimeParse(retweeted.CreatedAt);
1545 post.RetweetedId = retweeted.Id;
1547 post.TextFromApi = retweeted.Text;
1548 entities = retweeted.MergedEntities;
1549 sourceHtml = retweeted.Source;
1551 post.InReplyToStatusId = retweeted.InReplyToStatusId;
1552 post.InReplyToUser = retweeted.InReplyToScreenName;
1553 post.InReplyToUserId = status.InReplyToUserId;
1562 var tc = TabInformations.GetInstance().GetTabByType(MyCommon.TabUsageType.Favorites);
1563 post.IsFav = tc.Contains(retweeted.Id);
1566 if (retweeted.Coordinates != null)
1567 post.PostGeo = new PostClass.StatusGeo(retweeted.Coordinates.Coordinates[0], retweeted.Coordinates.Coordinates[1]);
1570 var user = retweeted.User;
1572 if (user == null || user.ScreenName == null || status.User.ScreenName == null) return null;
1574 post.UserId = user.Id;
1575 post.ScreenName = user.ScreenName;
1576 post.Nickname = user.Name.Trim();
1577 post.ImageUrl = user.ProfileImageUrlHttps;
1578 post.IsProtect = user.Protected;
1581 post.RetweetedBy = status.User.ScreenName;
1582 post.RetweetedByUserId = status.User.Id;
1583 post.IsMe = post.RetweetedBy.ToLowerInvariant().Equals(_uname);
1587 post.CreatedAt = MyCommon.DateTimeParse(status.CreatedAt);
1589 post.TextFromApi = status.Text;
1590 entities = status.MergedEntities;
1591 sourceHtml = status.Source;
1592 post.InReplyToStatusId = status.InReplyToStatusId;
1593 post.InReplyToUser = status.InReplyToScreenName;
1594 post.InReplyToUserId = status.InReplyToUserId;
1603 var tc = TabInformations.GetInstance().GetTabByType(MyCommon.TabUsageType.Favorites);
1604 post.IsFav = tc.Contains(post.StatusId) && TabInformations.GetInstance()[post.StatusId].IsFav;
1607 if (status.Coordinates != null)
1608 post.PostGeo = new PostClass.StatusGeo(status.Coordinates.Coordinates[0], status.Coordinates.Coordinates[1]);
1611 var user = status.User;
1613 if (user == null || user.ScreenName == null) return null;
1615 post.UserId = user.Id;
1616 post.ScreenName = user.ScreenName;
1617 post.Nickname = user.Name.Trim();
1618 post.ImageUrl = user.ProfileImageUrlHttps;
1619 post.IsProtect = user.Protected;
1620 post.IsMe = post.ScreenName.ToLowerInvariant().Equals(_uname);
1623 string textFromApi = post.TextFromApi;
1624 post.Text = CreateHtmlAnchor(textFromApi, post.ReplyToList, entities, post.Media);
1625 post.TextFromApi = textFromApi;
1626 post.TextFromApi = this.ReplaceTextFromApi(post.TextFromApi, entities);
1627 post.TextFromApi = WebUtility.HtmlDecode(post.TextFromApi);
1628 post.TextFromApi = post.TextFromApi.Replace("<3", "\u2661");
1630 post.QuoteStatusIds = GetQuoteTweetStatusIds(entities)
1631 .Where(x => x != post.StatusId && x != post.RetweetedId)
1632 .Distinct().ToArray();
1634 post.ExpandedUrls = entities.OfType<TwitterEntityUrl>()
1635 .Select(x => new PostClass.ExpandedUrlInfo(x.Url, x.ExpandedUrl))
1639 var source = ParseSource(sourceHtml);
1640 post.Source = source.Item1;
1641 post.SourceUri = source.Item2;
1643 post.IsReply = post.ReplyToList.Contains(_uname);
1644 post.IsExcludeReply = false;
1652 if (followerId.Count > 0) post.IsOwl = !followerId.Contains(post.UserId);
1660 /// ツイートに含まれる引用ツイートのURLからステータスIDを抽出
1662 public static IEnumerable<long> GetQuoteTweetStatusIds(IEnumerable<TwitterEntity> entities)
1664 var urls = entities.OfType<TwitterEntityUrl>().Select(x => x.ExpandedUrl);
1666 return GetQuoteTweetStatusIds(urls);
1669 public static IEnumerable<long> GetQuoteTweetStatusIds(IEnumerable<string> urls)
1671 foreach (var url in urls)
1673 var match = Twitter.StatusUrlRegex.Match(url);
1677 if (long.TryParse(match.Groups["StatusId"].Value, out statusId))
1678 yield return statusId;
1683 private long? CreatePostsFromJson(string content, MyCommon.WORKERTYPE gType, TabClass tab, bool read)
1685 TwitterStatus[] items;
1688 items = TwitterStatus.ParseJsonArray(content);
1690 catch(SerializationException ex)
1692 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
1693 throw new WebApiException("Json Parse Error(DataContractJsonSerializer)", content, ex);
1697 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
1698 throw new WebApiException("Invalid Json!", content, ex);
1701 long? minimumId = null;
1703 foreach (var status in items)
1705 PostClass post = null;
1706 post = CreatePostsFromStatusData(status);
1707 if (post == null) continue;
1709 if (minimumId == null || minimumId.Value > post.StatusId)
1710 minimumId = post.StatusId;
1717 if (TabInformations.GetInstance().ContainsKey(post.StatusId)) continue;
1721 if (tab.Contains(post.StatusId)) continue;
1726 if (gType != MyCommon.WORKERTYPE.UserTimeline &&
1727 post.RetweetedByUserId != null && this.noRTId.Contains(post.RetweetedByUserId.Value)) continue;
1730 if (post.IsMe && !read && _readOwnPost) post.IsRead = true;
1732 //非同期アイコン取得&StatusDictionaryに追加
1733 if (tab != null && tab.IsInnerStorageTabType)
1734 tab.AddPostToInnerStorage(post);
1736 TabInformations.GetInstance().AddPost(post);
1742 private long? CreatePostsFromSearchJson(string content, TabClass tab, bool read, int count, bool more)
1744 TwitterSearchResult items;
1747 items = TwitterSearchResult.ParseJson(content);
1749 catch (SerializationException ex)
1751 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
1752 throw new WebApiException("Json Parse Error(DataContractJsonSerializer)", content, ex);
1754 catch (Exception ex)
1756 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
1757 throw new WebApiException("Invalid Json!", content, ex);
1760 long? minimumId = null;
1762 foreach (var result in items.Statuses)
1764 PostClass post = null;
1765 post = CreatePostsFromStatusData(result);
1769 // Search API は相変わらずぶっ壊れたデータを返すことがあるため、必要なデータが欠如しているものは取得し直す
1772 post = this.GetStatusApi(read, result.Id);
1774 catch (WebApiException)
1780 if (minimumId == null || minimumId.Value > post.StatusId)
1781 minimumId = post.StatusId;
1783 if (!more && post.StatusId > tab.SinceId) tab.SinceId = post.StatusId;
1789 if (TabInformations.GetInstance().ContainsKey(post.StatusId)) continue;
1793 if (tab.Contains(post.StatusId)) continue;
1798 if ((post.IsMe && !read) && this._readOwnPost) post.IsRead = true;
1800 //非同期アイコン取得&StatusDictionaryに追加
1801 if (tab != null && tab.IsInnerStorageTabType)
1802 tab.AddPostToInnerStorage(post);
1804 TabInformations.GetInstance().AddPost(post);
1810 private void CreateFavoritePostsFromJson(string content, bool read)
1812 TwitterStatus[] item;
1815 item = TwitterStatus.ParseJsonArray(content);
1817 catch (SerializationException ex)
1819 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
1820 throw new WebApiException("Json Parse Error(DataContractJsonSerializer)", content, ex);
1822 catch (Exception ex)
1824 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
1825 throw new WebApiException("Invalid Json!", content, ex);
1828 var favTab = TabInformations.GetInstance().GetTabByType(MyCommon.TabUsageType.Favorites);
1830 foreach (var status in item)
1835 if (favTab.Contains(status.Id)) continue;
1838 var post = CreatePostsFromStatusData(status, true);
1839 if (post == null) continue;
1843 TabInformations.GetInstance().AddPost(post);
1847 public void GetListStatus(bool read,
1854 var count = GetApiResultCount(MyCommon.WORKERTYPE.List, more, startup);
1860 res = twCon.GetListsStatuses(tab.ListInfo.UserId, tab.ListInfo.Id, count, tab.OldestId, null, SettingCommon.Instance.IsListsIncludeRts, ref content);
1864 res = twCon.GetListsStatuses(tab.ListInfo.UserId, tab.ListInfo.Id, count, null, null, SettingCommon.Instance.IsListsIncludeRts, ref content);
1869 throw new WebApiException("Err:" + ex.Message, ex);
1872 this.CheckStatusCode(res, content);
1874 var minimumId = CreatePostsFromJson(content, MyCommon.WORKERTYPE.List, tab, read);
1876 if (minimumId != null)
1877 tab.OldestId = minimumId.Value;
1881 /// startStatusId からリプライ先の発言を辿る。発言は posts 以外からは検索しない。
1883 /// <returns>posts の中から検索されたリプライチェインの末端</returns>
1884 internal static PostClass FindTopOfReplyChain(IDictionary<Int64, PostClass> posts, Int64 startStatusId)
1886 if (!posts.ContainsKey(startStatusId))
1887 throw new ArgumentException("startStatusId (" + startStatusId + ") が posts の中から見つかりませんでした。", nameof(startStatusId));
1889 var nextPost = posts[startStatusId];
1890 while (nextPost.InReplyToStatusId != null)
1892 if (!posts.ContainsKey(nextPost.InReplyToStatusId.Value))
1894 nextPost = posts[nextPost.InReplyToStatusId.Value];
1900 public void GetRelatedResult(bool read, TabClass tab)
1902 var relPosts = new Dictionary<Int64, PostClass>();
1903 if (tab.RelationTargetPost.TextFromApi.Contains("@") && tab.RelationTargetPost.InReplyToStatusId == null)
1906 var p = TabInformations.GetInstance()[tab.RelationTargetPost.StatusId];
1907 if (p != null && p.InReplyToStatusId != null)
1909 tab.RelationTargetPost = p;
1913 p = this.GetStatusApi(read, tab.RelationTargetPost.StatusId);
1914 tab.RelationTargetPost = p;
1917 relPosts.Add(tab.RelationTargetPost.StatusId, tab.RelationTargetPost);
1919 Exception lastException = null;
1921 // in_reply_to_status_id を使用してリプライチェインを辿る
1922 var nextPost = FindTopOfReplyChain(relPosts, tab.RelationTargetPost.StatusId);
1924 while (nextPost.InReplyToStatusId != null && loopCount++ <= 20)
1926 var inReplyToId = nextPost.InReplyToStatusId.Value;
1928 var inReplyToPost = TabInformations.GetInstance()[inReplyToId];
1929 if (inReplyToPost == null)
1933 inReplyToPost = this.GetStatusApi(read, inReplyToId);
1935 catch (WebApiException ex)
1942 relPosts.Add(inReplyToPost.StatusId, inReplyToPost);
1944 nextPost = FindTopOfReplyChain(relPosts, nextPost.StatusId);
1947 //MRTとかに対応のためツイート内にあるツイートを指すURLを取り込む
1948 var text = tab.RelationTargetPost.Text;
1949 var ma = Twitter.StatusUrlRegex.Matches(text).Cast<Match>()
1950 .Concat(Twitter.ThirdPartyStatusUrlRegex.Matches(text).Cast<Match>());
1951 foreach (var _match in ma)
1954 if (Int64.TryParse(_match.Groups["StatusId"].Value, out _statusId))
1956 if (relPosts.ContainsKey(_statusId))
1959 var p = TabInformations.GetInstance()[_statusId];
1964 p = this.GetStatusApi(read, _statusId);
1966 catch (WebApiException ex)
1974 relPosts.Add(p.StatusId, p);
1978 relPosts.Values.ToList().ForEach(p =>
1980 if (p.IsMe && !read && this._readOwnPost)
1985 tab.AddPostToInnerStorage(p);
1988 if (lastException != null)
1989 throw new WebApiException(lastException.Message, lastException);
1992 public void GetSearch(bool read,
1998 var count = GetApiResultCount(MyCommon.WORKERTYPE.PublicSearch, more, false);
2000 long? sinceId = null;
2003 maxId = tab.OldestId - 1;
2007 sinceId = tab.SinceId;
2012 // TODO:一時的に40>100件に 件数変更UI作成の必要あり
2013 res = twCon.Search(tab.SearchWords, tab.SearchLang, count, maxId, sinceId, ref content);
2017 throw new WebApiException("Err:" + ex.Message, ex);
2021 case HttpStatusCode.BadRequest:
2022 throw new WebApiException("Invalid query", content);
2023 case HttpStatusCode.NotFound:
2024 throw new WebApiException("Invalid query", content);
2025 case HttpStatusCode.PaymentRequired: //API Documentには420と書いてあるが、該当コードがないので402にしてある
2026 throw new WebApiException("Search API Limit?", content);
2027 case HttpStatusCode.OK:
2030 throw new WebApiException("Err:" + res.ToString() + "(" + MethodBase.GetCurrentMethod().Name + ")", content);
2033 if (!TabInformations.GetInstance().ContainsTab(tab))
2036 var minimumId = this.CreatePostsFromSearchJson(content, tab, read, count, more);
2038 if (minimumId != null)
2039 tab.OldestId = minimumId.Value;
2042 private void CreateDirectMessagesFromJson(string content, MyCommon.WORKERTYPE gType, bool read)
2044 TwitterDirectMessage[] item;
2047 if (gType == MyCommon.WORKERTYPE.UserStream)
2049 item = new[] { TwitterStreamEventDirectMessage.ParseJson(content).DirectMessage };
2053 item = TwitterDirectMessage.ParseJsonArray(content);
2056 catch(SerializationException ex)
2058 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
2059 throw new WebApiException("Json Parse Error(DataContractJsonSerializer)", content, ex);
2063 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
2064 throw new WebApiException("Invalid Json!", content, ex);
2067 foreach (var message in item)
2069 var post = new PostClass();
2072 post.StatusId = message.Id;
2073 if (gType != MyCommon.WORKERTYPE.UserStream)
2075 if (gType == MyCommon.WORKERTYPE.DirectMessegeRcv)
2077 if (minDirectmessage > post.StatusId) minDirectmessage = post.StatusId;
2081 if (minDirectmessageSent > post.StatusId) minDirectmessageSent = post.StatusId;
2088 if (TabInformations.GetInstance().GetTabByType(MyCommon.TabUsageType.DirectMessage).Contains(post.StatusId)) continue;
2092 post.CreatedAt = MyCommon.DateTimeParse(message.CreatedAt);
2094 var textFromApi = message.Text;
2096 post.Text = CreateHtmlAnchor(textFromApi, post.ReplyToList, message.Entities, post.Media);
2097 post.TextFromApi = this.ReplaceTextFromApi(textFromApi, message.Entities);
2098 post.TextFromApi = WebUtility.HtmlDecode(post.TextFromApi);
2099 post.TextFromApi = post.TextFromApi.Replace("<3", "\u2661");
2102 post.QuoteStatusIds = GetQuoteTweetStatusIds(message.Entities).Distinct().ToArray();
2104 post.ExpandedUrls = message.Entities.OfType<TwitterEntityUrl>()
2105 .Select(x => new PostClass.ExpandedUrlInfo(x.Url, x.ExpandedUrl))
2110 if (gType == MyCommon.WORKERTYPE.UserStream)
2112 if (twCon.AuthenticatedUsername.Equals(message.Recipient.ScreenName, StringComparison.CurrentCultureIgnoreCase))
2114 user = message.Sender;
2120 user = message.Recipient;
2127 if (gType == MyCommon.WORKERTYPE.DirectMessegeRcv)
2129 user = message.Sender;
2135 user = message.Recipient;
2141 post.UserId = user.Id;
2142 post.ScreenName = user.ScreenName;
2143 post.Nickname = user.Name.Trim();
2144 post.ImageUrl = user.ProfileImageUrlHttps;
2145 post.IsProtect = user.Protected;
2149 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
2150 MessageBox.Show("Parse Error(CreateDirectMessagesFromJson)");
2155 if (post.IsMe && !read && _readOwnPost) post.IsRead = true;
2156 post.IsReply = false;
2157 post.IsExcludeReply = false;
2160 var dmTab = TabInformations.GetInstance().GetTabByType(MyCommon.TabUsageType.DirectMessage);
2161 dmTab.AddPostToInnerStorage(post);
2165 public void GetDirectMessageApi(bool read,
2166 MyCommon.WORKERTYPE gType,
2169 this.CheckAccountState();
2170 this.CheckAccessLevel(TwitterApiAccessLevel.ReadWriteAndDirectMessage);
2174 var count = GetApiResultCount(gType, more, false);
2178 if (gType == MyCommon.WORKERTYPE.DirectMessegeRcv)
2182 res = twCon.DirectMessages(count, minDirectmessage, null, ref content);
2186 res = twCon.DirectMessages(count, null, null, ref content);
2193 res = twCon.DirectMessagesSent(count, minDirectmessageSent, null, ref content);
2197 res = twCon.DirectMessagesSent(count, null, null, ref content);
2203 throw new WebApiException("Err:" + ex.Message, ex);
2206 this.CheckStatusCode(res, content);
2208 CreateDirectMessagesFromJson(content, gType, read);
2211 public void GetFavoritesApi(bool read,
2214 this.CheckAccountState();
2218 var count = GetApiResultCount(MyCommon.WORKERTYPE.Favorites, more, false);
2222 res = twCon.Favorites(count, ref content);
2226 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
2229 this.CheckStatusCode(res, content);
2231 CreateFavoritePostsFromJson(content, read);
2234 private string ReplaceTextFromApi(string text, TwitterEntities entities)
2236 if (entities != null)
2238 if (entities.Urls != null)
2240 foreach (var m in entities.Urls)
2242 if (!string.IsNullOrEmpty(m.DisplayUrl)) text = text.Replace(m.Url, m.DisplayUrl);
2245 if (entities.Media != null)
2247 foreach (var m in entities.Media)
2249 if (!string.IsNullOrEmpty(m.DisplayUrl)) text = text.Replace(m.Url, m.DisplayUrl);
2259 /// <exception cref="WebApiException"/>
2260 public void RefreshFollowerIds()
2262 if (MyCommon._endingFlag) return;
2265 var newFollowerIds = new HashSet<long>();
2268 var ret = this.GetFollowerIdsApi(ref cursor);
2269 newFollowerIds.UnionWith(ret.Ids);
2270 cursor = ret.NextCursor;
2271 } while (cursor != 0);
2273 this.followerId = newFollowerIds;
2274 TabInformations.GetInstance().RefreshOwl(this.followerId);
2276 this._GetFollowerResult = true;
2279 public bool GetFollowersSuccess
2283 return _GetFollowerResult;
2287 private TwitterIds GetFollowerIdsApi(ref long cursor)
2289 this.CheckAccountState();
2295 res = twCon.FollowerIds(cursor, ref content);
2299 throw new WebApiException("Err:" + e.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", e);
2302 this.CheckStatusCode(res, content);
2306 var ret = TwitterIds.ParseJson(content);
2308 if (ret.Ids == null)
2310 var ex = new WebApiException("Err: ret.id == null (GetFollowerIdsApi)", content);
2311 MyCommon.ExceptionOut(ex);
2317 catch(SerializationException e)
2319 var ex = new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, e);
2320 MyCommon.TraceOut(ex);
2325 var ex = new WebApiException("Err:Invalid Json!", content, e);
2326 MyCommon.TraceOut(ex);
2332 /// RT 非表示ユーザーを更新します
2334 /// <exception cref="WebApiException"/>
2335 public void RefreshNoRetweetIds()
2337 if (MyCommon._endingFlag) return;
2339 this.noRTId = this.NoRetweetIdsApi();
2341 this._GetNoRetweetResult = true;
2344 private long[] NoRetweetIdsApi()
2346 this.CheckAccountState();
2352 res = twCon.NoRetweetIds(ref content);
2356 throw new WebApiException("Err:" + e.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", e);
2359 this.CheckStatusCode(res, content);
2363 return MyCommon.CreateDataFromJson<long[]>(content);
2365 catch(SerializationException e)
2367 var ex = new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, e);
2368 MyCommon.TraceOut(ex);
2373 var ex = new WebApiException("Err:Invalid Json!", content, e);
2374 MyCommon.TraceOut(ex);
2379 public bool GetNoRetweetSuccess
2383 return _GetNoRetweetResult;
2388 /// t.co の文字列長などの設定情報を更新します
2390 /// <exception cref="WebApiException"/>
2391 public void RefreshConfiguration()
2393 this.Configuration = this.ConfigurationApi();
2396 private TwitterConfiguration ConfigurationApi()
2402 res = twCon.GetConfiguration(ref content);
2406 throw new WebApiException("Err:" + e.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", e);
2409 this.CheckStatusCode(res, content);
2413 return TwitterConfiguration.ParseJson(content);
2415 catch(SerializationException e)
2417 var ex = new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, e);
2418 MyCommon.TraceOut(ex);
2423 var ex = new WebApiException("Err:Invalid Json!", content, e);
2424 MyCommon.TraceOut(ex);
2429 public void GetListsApi()
2431 this.CheckAccountState();
2434 IEnumerable<ListElement> lists;
2439 res = twCon.GetLists(this.Username, ref content);
2441 catch (Exception ex)
2443 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
2446 this.CheckStatusCode(res, content);
2450 lists = TwitterList.ParseJsonArray(content)
2451 .Select(x => new ListElement(x, this));
2453 catch (SerializationException ex)
2455 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
2456 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
2458 catch (Exception ex)
2460 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
2461 throw new WebApiException("Err:Invalid Json!", content, ex);
2466 res = twCon.GetListsSubscriptions(this.Username, ref content);
2468 catch (Exception ex)
2470 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
2473 this.CheckStatusCode(res, content);
2477 lists = lists.Concat(TwitterList.ParseJsonArray(content)
2478 .Select(x => new ListElement(x, this)));
2480 catch (SerializationException ex)
2482 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
2483 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
2485 catch (Exception ex)
2487 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
2488 throw new WebApiException("Err:Invalid Json!", content, ex);
2491 TabInformations.GetInstance().SubscribableLists = lists.ToList();
2494 public void DeleteList(string list_id)
2501 res = twCon.DeleteListID(this.Username, list_id, ref content);
2505 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
2508 this.CheckStatusCode(res, content);
2511 public ListElement EditList(string list_id, string new_name, bool isPrivate, string description)
2518 res = twCon.UpdateListID(this.Username, list_id, new_name, isPrivate, description, ref content);
2522 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
2525 this.CheckStatusCode(res, content);
2529 var le = TwitterList.ParseJson(content);
2530 return new ListElement(le, this);
2532 catch(SerializationException ex)
2534 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
2535 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
2539 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
2540 throw new WebApiException("Err:Invalid Json!", content, ex);
2544 public long GetListMembers(string list_id, List<UserInfo> lists, long cursor)
2546 this.CheckAccountState();
2552 res = twCon.GetListMembers(this.Username, list_id, cursor, ref content);
2556 throw new WebApiException("Err:" + ex.Message);
2559 this.CheckStatusCode(res, content);
2563 var users = TwitterUsers.ParseJson(content);
2564 Array.ForEach<TwitterUser>(
2566 u => lists.Add(new UserInfo(u)));
2568 return users.NextCursor;
2570 catch(SerializationException ex)
2572 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
2573 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
2577 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
2578 throw new WebApiException("Err:Invalid Json!", content, ex);
2582 public void CreateListApi(string listName, bool isPrivate, string description)
2584 this.CheckAccountState();
2590 res = twCon.CreateLists(listName, isPrivate, description, ref content);
2594 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
2597 this.CheckStatusCode(res, content);
2601 var le = TwitterList.ParseJson(content);
2602 TabInformations.GetInstance().SubscribableLists.Add(new ListElement(le, this));
2604 catch(SerializationException ex)
2606 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
2607 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
2611 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
2612 throw new WebApiException("Err:Invalid Json!", content, ex);
2616 public bool ContainsUserAtList(string listId, string user)
2618 this.CheckAccountState();
2625 res = this.twCon.ShowListMember(listId, user, ref content);
2629 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
2632 if (res == HttpStatusCode.NotFound)
2637 this.CheckStatusCode(res, content);
2641 TwitterUser.ParseJson(content);
2650 public void AddUserToList(string listId, string user)
2657 res = twCon.CreateListMembers(listId, user, ref content);
2661 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
2664 this.CheckStatusCode(res, content);
2667 public void RemoveUserToList(string listId, string user)
2674 res = twCon.DeleteListMembers(listId, user, ref content);
2678 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
2681 this.CheckStatusCode(res, content);
2684 public string CreateHtmlAnchor(string text, List<string> AtList, TwitterEntities entities, List<MediaInfo> media)
2686 if (entities != null)
2688 if (entities.Hashtags != null)
2692 this._hashList.AddRange(entities.Hashtags.Select(x => "#" + x.Text));
2695 if (entities.UserMentions != null)
2697 foreach (var ent in entities.UserMentions)
2699 var screenName = ent.ScreenName.ToLowerInvariant();
2700 if (!AtList.Contains(screenName))
2701 AtList.Add(screenName);
2704 if (entities.Media != null)
2708 foreach (var ent in entities.Media)
2710 if (!media.Any(x => x.Url == ent.MediaUrl))
2712 if (ent.VideoInfo != null &&
2713 ent.Type == "animated_gif" || ent.Type == "video")
2715 //var videoUrl = ent.VideoInfo.Variants
2716 // .Where(v => v.ContentType == "video/mp4")
2717 // .OrderByDescending(v => v.Bitrate)
2718 // .Select(v => v.Url).FirstOrDefault();
2719 media.Add(new MediaInfo(ent.MediaUrl, ent.AltText, ent.ExpandedUrl));
2722 media.Add(new MediaInfo(ent.MediaUrl, ent.AltText, videoUrl: null));
2729 // PostClass.ExpandedUrlInfo を使用して非同期に URL 展開を行うためここでは expanded_url を使用しない
2730 text = TweetFormatter.AutoLinkHtml(text, entities, keepTco: true);
2732 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>");
2733 text = PreProcessUrl(text); //IDN置換
2738 private static readonly Uri SourceUriBase = new Uri("https://twitter.com/");
2741 /// Twitter APIから得たHTML形式のsource文字列を分析し、source名とURLに分離します
2743 public static Tuple<string, Uri> ParseSource(string sourceHtml)
2745 if (string.IsNullOrEmpty(sourceHtml))
2746 return Tuple.Create<string, Uri>("", null);
2751 // sourceHtmlの例: <a href="http://twitter.com" rel="nofollow">Twitter Web Client</a>
2753 var match = Regex.Match(sourceHtml, "^<a href=\"(?<uri>.+?)\".*?>(?<text>.+)</a>$", RegexOptions.IgnoreCase);
2756 sourceText = WebUtility.HtmlDecode(match.Groups["text"].Value);
2759 var uriStr = WebUtility.HtmlDecode(match.Groups["uri"].Value);
2760 sourceUri = new Uri(SourceUriBase, uriStr);
2762 catch (UriFormatException)
2769 sourceText = WebUtility.HtmlDecode(sourceHtml);
2773 return Tuple.Create(sourceText, sourceUri);
2776 public TwitterApiStatus GetInfoApi()
2778 if (Twitter.AccountState != MyCommon.ACCOUNT_STATE.Valid) return null;
2780 if (MyCommon._endingFlag) return null;
2786 res = twCon.RateLimitStatus(ref content);
2790 this.ResetApiStatus();
2794 this.CheckStatusCode(res, content);
2798 MyCommon.TwitterApiInfo.UpdateFromJson(content);
2799 return MyCommon.TwitterApiInfo;
2801 catch (Exception ex)
2803 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
2804 MyCommon.TwitterApiInfo.Reset();
2810 /// ブロック中のユーザーを更新します
2812 /// <exception cref="WebApiException"/>
2813 public void RefreshBlockIds()
2815 if (MyCommon._endingFlag) return;
2818 var newBlockIds = new HashSet<long>();
2821 var ret = this.GetBlockIdsApi(cursor);
2822 newBlockIds.UnionWith(ret.Ids);
2823 cursor = ret.NextCursor;
2824 } while (cursor != 0);
2826 newBlockIds.Remove(this.UserId); // 元のソースにあったので一応残しておく
2828 TabInformations.GetInstance().BlockIds = newBlockIds;
2831 public TwitterIds GetBlockIdsApi(long cursor)
2833 this.CheckAccountState();
2839 res = twCon.GetBlockUserIds(ref content, cursor);
2843 throw new WebApiException("Err:" + e.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", e);
2846 this.CheckStatusCode(res, content);
2850 return TwitterIds.ParseJson(content);
2852 catch(SerializationException e)
2854 var ex = new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, e);
2855 MyCommon.TraceOut(ex);
2860 var ex = new WebApiException("Err:Invalid Json!", content, e);
2861 MyCommon.TraceOut(ex);
2867 /// ミュート中のユーザーIDを更新します
2869 /// <exception cref="WebApiException"/>
2870 public async Task RefreshMuteUserIdsAsync()
2872 if (MyCommon._endingFlag) return;
2874 var ids = await TwitterIds.GetAllItemsAsync(this.GetMuteUserIdsApiAsync)
2875 .ConfigureAwait(false);
2877 TabInformations.GetInstance().MuteUserIds = new HashSet<long>(ids);
2880 public async Task<TwitterIds> GetMuteUserIdsApiAsync(long cursor)
2886 var res = await Task.Run(() => twCon.GetMuteUserIds(ref content, cursor))
2887 .ConfigureAwait(false);
2889 this.CheckStatusCode(res, content);
2891 return TwitterIds.ParseJson(content);
2893 catch (WebException ex)
2895 var ex2 = new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", content, ex);
2896 MyCommon.TraceOut(ex2);
2899 catch (SerializationException ex)
2901 var ex2 = new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
2902 MyCommon.TraceOut(ex2);
2907 public string[] GetHashList()
2912 hashArray = _hashList.ToArray();
2918 public string AccessToken
2922 return twCon.AccessToken;
2926 public string AccessTokenSecret
2930 return twCon.AccessTokenSecret;
2934 private void CheckAccountState()
2936 if (Twitter.AccountState != MyCommon.ACCOUNT_STATE.Valid)
2937 throw new WebApiException("Auth error. Check your account");
2940 private void CheckAccessLevel(TwitterApiAccessLevel accessLevelFlags)
2942 if (!this.AccessLevel.HasFlag(accessLevelFlags))
2943 throw new WebApiException("Auth Err:try to re-authorization.");
2946 private void CheckStatusCode(HttpStatusCode httpStatus, string responseText,
2947 [CallerMemberName] string callerMethodName = "")
2949 if (httpStatus == HttpStatusCode.OK)
2951 Twitter.AccountState = MyCommon.ACCOUNT_STATE.Valid;
2955 if (string.IsNullOrWhiteSpace(responseText))
2957 if (httpStatus == HttpStatusCode.Unauthorized)
2958 Twitter.AccountState = MyCommon.ACCOUNT_STATE.Invalid;
2960 throw new WebApiException("Err:" + httpStatus + "(" + callerMethodName + ")");
2965 var errors = TwitterError.ParseJson(responseText).Errors;
2966 if (errors == null || !errors.Any())
2968 throw new WebApiException("Err:" + httpStatus + "(" + callerMethodName + ")", responseText);
2971 foreach (var error in errors)
2973 if (error.Code == TwitterErrorCode.InvalidToken ||
2974 error.Code == TwitterErrorCode.SuspendedAccount)
2976 Twitter.AccountState = MyCommon.ACCOUNT_STATE.Invalid;
2980 throw new WebApiException("Err:" + string.Join(",", errors.Select(x => x.ToString())) + "(" + callerMethodName + ")", responseText);
2982 catch (SerializationException) { }
2984 throw new WebApiException("Err:" + httpStatus + "(" + callerMethodName + ")", responseText);
2987 public int GetTextLengthRemain(string postText)
2989 var matchDm = Twitter.DMSendTextRegex.Match(postText);
2990 if (matchDm.Success)
2991 return this.GetTextLengthRemainInternal(matchDm.Groups["body"].Value, isDm: true);
2993 return this.GetTextLengthRemainInternal(postText, isDm: false);
2996 private int GetTextLengthRemainInternal(string postText, bool isDm)
3001 while (pos < postText.Length)
3005 if (char.IsSurrogatePair(postText, pos))
3006 pos += 2; // サロゲートペアの場合は2文字分進める
3011 var urls = TweetExtractor.ExtractUrls(postText);
3012 foreach (var url in urls)
3014 var shortUrlLength = url.StartsWith("https://", StringComparison.OrdinalIgnoreCase)
3015 ? this.Configuration.ShortUrlLengthHttps
3016 : this.Configuration.ShortUrlLength;
3018 textLength += shortUrlLength - url.Length;
3022 return this.Configuration.DmTextCharacterLimit - textLength;
3024 return 140 - textLength;
3028 #region "UserStream"
3029 private string trackWord_ = "";
3030 public string TrackWord
3041 private bool allAtReply_ = false;
3042 public bool AllAtReply
3050 allAtReply_ = value;
3054 public event EventHandler NewPostFromStream;
3055 public event EventHandler UserStreamStarted;
3056 public event EventHandler UserStreamStopped;
3057 public event EventHandler<PostDeletedEventArgs> PostDeleted;
3058 public event EventHandler<UserStreamEventReceivedEventArgs> UserStreamEventReceived;
3059 private DateTime _lastUserstreamDataReceived;
3060 private TwitterUserstream userStream;
3062 public class FormattedEvent
3064 public MyCommon.EVENTTYPE Eventtype { get; set; }
3065 public DateTime CreatedAt { get; set; }
3066 public string Event { get; set; }
3067 public string Username { get; set; }
3068 public string Target { get; set; }
3069 public Int64 Id { get; set; }
3070 public bool IsMe { get; set; }
3073 public List<FormattedEvent> storedEvent_ = new List<FormattedEvent>();
3074 public List<FormattedEvent> StoredEvent
3078 return storedEvent_;
3082 storedEvent_ = value;
3086 private readonly IReadOnlyDictionary<string, MyCommon.EVENTTYPE> eventTable = new Dictionary<string, MyCommon.EVENTTYPE>
3088 ["favorite"] = MyCommon.EVENTTYPE.Favorite,
3089 ["unfavorite"] = MyCommon.EVENTTYPE.Unfavorite,
3090 ["follow"] = MyCommon.EVENTTYPE.Follow,
3091 ["list_member_added"] = MyCommon.EVENTTYPE.ListMemberAdded,
3092 ["list_member_removed"] = MyCommon.EVENTTYPE.ListMemberRemoved,
3093 ["block"] = MyCommon.EVENTTYPE.Block,
3094 ["unblock"] = MyCommon.EVENTTYPE.Unblock,
3095 ["user_update"] = MyCommon.EVENTTYPE.UserUpdate,
3096 ["deleted"] = MyCommon.EVENTTYPE.Deleted,
3097 ["list_created"] = MyCommon.EVENTTYPE.ListCreated,
3098 ["list_destroyed"] = MyCommon.EVENTTYPE.ListDestroyed,
3099 ["list_updated"] = MyCommon.EVENTTYPE.ListUpdated,
3100 ["unfollow"] = MyCommon.EVENTTYPE.Unfollow,
3101 ["list_user_subscribed"] = MyCommon.EVENTTYPE.ListUserSubscribed,
3102 ["list_user_unsubscribed"] = MyCommon.EVENTTYPE.ListUserUnsubscribed,
3103 ["mute"] = MyCommon.EVENTTYPE.Mute,
3104 ["unmute"] = MyCommon.EVENTTYPE.Unmute,
3105 ["quoted_tweet"] = MyCommon.EVENTTYPE.QuotedTweet,
3108 public bool IsUserstreamDataReceived
3112 return DateTime.Now.Subtract(this._lastUserstreamDataReceived).TotalSeconds < 31;
3116 private void userStream_StatusArrived(string line)
3118 this._lastUserstreamDataReceived = DateTime.Now;
3119 if (string.IsNullOrEmpty(line)) return;
3121 if (line.First() != '{' || line.Last() != '}')
3123 MyCommon.TraceOut("Invalid JSON (StatusArrived):" + Environment.NewLine + line);
3131 using (var jsonReader = JsonReaderWriterFactory.CreateJsonReader(Encoding.UTF8.GetBytes(line), XmlDictionaryReaderQuotas.Max))
3133 var xElm = XElement.Load(jsonReader);
3134 if (xElm.Element("friends") != null)
3136 Debug.WriteLine("friends");
3139 else if (xElm.Element("delete") != null)
3141 Debug.WriteLine("delete");
3144 if ((idElm = xElm.Element("delete").Element("direct_message")?.Element("id")) != null)
3147 long.TryParse(idElm.Value, out id);
3149 this.PostDeleted?.Invoke(this, new PostDeletedEventArgs(id));
3151 else if ((idElm = xElm.Element("delete").Element("status")?.Element("id")) != null)
3154 long.TryParse(idElm.Value, out id);
3156 this.PostDeleted?.Invoke(this, new PostDeletedEventArgs(id));
3160 MyCommon.TraceOut("delete:" + line);
3163 for (int i = this.StoredEvent.Count - 1; i >= 0; i--)
3165 var sEvt = this.StoredEvent[i];
3166 if (sEvt.Id == id && (sEvt.Event == "favorite" || sEvt.Event == "unfavorite"))
3168 this.StoredEvent.RemoveAt(i);
3173 else if (xElm.Element("limit") != null)
3175 Debug.WriteLine(line);
3178 else if (xElm.Element("event") != null)
3180 Debug.WriteLine("event: " + xElm.Element("event").Value);
3181 CreateEventFromJson(line);
3184 else if (xElm.Element("direct_message") != null)
3186 Debug.WriteLine("direct_message");
3189 else if (xElm.Element("retweeted_status") != null)
3191 var sourceUserId = xElm.XPathSelectElement("/user/id_str").Value;
3192 var targetUserId = xElm.XPathSelectElement("/retweeted_status/user/id_str").Value;
3194 // 自分に関係しないリツイートの場合は無視する
3195 var selfUserId = this.UserId.ToString();
3196 if (sourceUserId == selfUserId || targetUserId == selfUserId)
3198 // 公式 RT をイベントとしても扱う
3199 var evt = CreateEventFromRetweet(xElm);
3202 this.StoredEvent.Insert(0, evt);
3204 this.UserStreamEventReceived?.Invoke(this, new UserStreamEventReceivedEventArgs(evt));
3208 // 従来通り公式 RT の表示も行うため return しない
3210 else if (xElm.Element("scrub_geo") != null)
3214 TabInformations.GetInstance().ScrubGeoReserve(long.Parse(xElm.Element("scrub_geo").Element("user_id").Value),
3215 long.Parse(xElm.Element("scrub_geo").Element("up_to_status_id").Value));
3219 MyCommon.TraceOut("scrub_geo:" + line);
3227 CreateDirectMessagesFromJson(line, MyCommon.WORKERTYPE.UserStream, false);
3231 CreatePostsFromJson("[" + line + "]", MyCommon.WORKERTYPE.Timeline, null, false);
3234 catch (WebApiException ex)
3236 MyCommon.TraceOut(ex);
3239 catch(NullReferenceException)
3241 MyCommon.TraceOut("NullRef StatusArrived: " + line);
3244 this.NewPostFromStream?.Invoke(this, EventArgs.Empty);
3248 /// UserStreamsから受信した公式RTをイベントに変換します
3250 private FormattedEvent CreateEventFromRetweet(XElement xElm)
3252 return new FormattedEvent
3254 Eventtype = MyCommon.EVENTTYPE.Retweet,
3256 CreatedAt = MyCommon.DateTimeParse(xElm.XPathSelectElement("/created_at").Value),
3257 IsMe = xElm.XPathSelectElement("/user/id_str").Value == this.UserId.ToString(),
3258 Username = xElm.XPathSelectElement("/user/screen_name").Value,
3259 Target = string.Format("@{0}:{1}", new[]
3261 xElm.XPathSelectElement("/retweeted_status/user/screen_name").Value,
3262 WebUtility.HtmlDecode(xElm.XPathSelectElement("/retweeted_status/text").Value),
3264 Id = long.Parse(xElm.XPathSelectElement("/retweeted_status/id_str").Value),
3268 private void CreateEventFromJson(string content)
3270 TwitterStreamEvent eventData = null;
3273 eventData = TwitterStreamEvent.ParseJson(content);
3275 catch(SerializationException ex)
3277 MyCommon.TraceOut(ex, "Event Serialize Exception!" + Environment.NewLine + content);
3281 MyCommon.TraceOut(ex, "Event Exception!" + Environment.NewLine + content);
3284 var evt = new FormattedEvent();
3285 evt.CreatedAt = MyCommon.DateTimeParse(eventData.CreatedAt);
3286 evt.Event = eventData.Event;
3287 evt.Username = eventData.Source.ScreenName;
3288 evt.IsMe = evt.Username.ToLowerInvariant().Equals(this.Username.ToLowerInvariant());
3290 MyCommon.EVENTTYPE eventType;
3291 eventTable.TryGetValue(eventData.Event, out eventType);
3292 evt.Eventtype = eventType;
3294 TwitterStreamEvent<TwitterStatus> tweetEvent;
3296 switch (eventData.Event)
3298 case "access_revoked":
3299 case "access_unrevoked":
3301 case "user_suspend":
3304 if (eventData.Target.ScreenName.ToLowerInvariant().Equals(_uname))
3306 if (!this.followerId.Contains(eventData.Source.Id)) this.followerId.Add(eventData.Source.Id);
3310 return; //Block後のUndoをすると、SourceとTargetが逆転したfollowイベントが帰ってくるため。
3315 evt.Target = "@" + eventData.Target.ScreenName;
3317 case "favorited_retweet":
3318 case "retweeted_retweet":
3322 tweetEvent = TwitterStreamEvent<TwitterStatus>.ParseJson(content);
3323 evt.Target = "@" + tweetEvent.TargetObject.User.ScreenName + ":" + WebUtility.HtmlDecode(tweetEvent.TargetObject.Text);
3324 evt.Id = tweetEvent.TargetObject.Id;
3326 if (SettingCommon.Instance.IsRemoveSameEvent)
3328 if (this.StoredEvent.Any(ev => ev.Username == evt.Username && ev.Eventtype == evt.Eventtype && ev.Target == evt.Target))
3332 var tabinfo = TabInformations.GetInstance();
3335 var statusId = tweetEvent.TargetObject.Id;
3336 if (!tabinfo.Posts.TryGetValue(statusId, out post))
3339 if (eventData.Event == "favorite")
3341 var favTab = tabinfo.GetTabByType(MyCommon.TabUsageType.Favorites);
3342 if (!favTab.Contains(post.StatusId))
3343 favTab.AddPostImmediately(post.StatusId, post.IsRead);
3345 if (tweetEvent.Source.Id == this.UserId)
3349 else if (tweetEvent.Target.Id == this.UserId)
3351 post.FavoritedCount++;
3353 if (SettingCommon.Instance.FavEventUnread)
3354 tabinfo.SetReadAllTab(post.StatusId, read: false);
3359 if (tweetEvent.Source.Id == this.UserId)
3363 else if (tweetEvent.Target.Id == this.UserId)
3365 post.FavoritedCount = Math.Max(0, post.FavoritedCount - 1);
3369 case "quoted_tweet":
3370 if (evt.IsMe) return;
3372 tweetEvent = TwitterStreamEvent<TwitterStatus>.ParseJson(content);
3373 evt.Target = "@" + tweetEvent.TargetObject.User.ScreenName + ":" + WebUtility.HtmlDecode(tweetEvent.TargetObject.Text);
3374 evt.Id = tweetEvent.TargetObject.Id;
3376 if (SettingCommon.Instance.IsRemoveSameEvent)
3378 if (this.StoredEvent.Any(ev => ev.Username == evt.Username && ev.Eventtype == evt.Eventtype && ev.Target == evt.Target))
3382 case "list_member_added":
3383 case "list_member_removed":
3384 case "list_created":
3385 case "list_destroyed":
3386 case "list_updated":
3387 case "list_user_subscribed":
3388 case "list_user_unsubscribed":
3389 var listEvent = TwitterStreamEvent<TwitterList>.ParseJson(content);
3390 evt.Target = listEvent.TargetObject.FullName;
3393 if (!TabInformations.GetInstance().BlockIds.Contains(eventData.Target.Id)) TabInformations.GetInstance().BlockIds.Add(eventData.Target.Id);
3397 if (TabInformations.GetInstance().BlockIds.Contains(eventData.Target.Id)) TabInformations.GetInstance().BlockIds.Remove(eventData.Target.Id);
3406 evt.Target = "@" + eventData.Target.ScreenName;
3407 if (!TabInformations.GetInstance().MuteUserIds.Contains(eventData.Target.Id))
3409 TabInformations.GetInstance().MuteUserIds.Add(eventData.Target.Id);
3413 evt.Target = "@" + eventData.Target.ScreenName;
3414 if (TabInformations.GetInstance().MuteUserIds.Contains(eventData.Target.Id))
3416 TabInformations.GetInstance().MuteUserIds.Remove(eventData.Target.Id);
3421 MyCommon.TraceOut("Unknown Event:" + evt.Event + Environment.NewLine + content);
3424 this.StoredEvent.Insert(0, evt);
3426 this.UserStreamEventReceived?.Invoke(this, new UserStreamEventReceivedEventArgs(evt));
3429 private void userStream_Started()
3431 this.UserStreamStarted?.Invoke(this, EventArgs.Empty);
3434 private void userStream_Stopped()
3436 this.UserStreamStopped?.Invoke(this, EventArgs.Empty);
3439 public bool UserStreamEnabled
3443 return userStream == null ? false : userStream.Enabled;
3447 public void StartUserStream()
3449 if (userStream != null)
3453 userStream = new TwitterUserstream(twCon);
3454 userStream.StatusArrived += userStream_StatusArrived;
3455 userStream.Started += userStream_Started;
3456 userStream.Stopped += userStream_Stopped;
3457 userStream.Start(this.AllAtReply, this.TrackWord);
3460 public void StopUserStream()
3462 userStream?.Dispose();
3464 if (!MyCommon._endingFlag)
3466 this.UserStreamStopped?.Invoke(this, EventArgs.Empty);
3470 public void ReconnectUserStream()
3472 if (userStream != null)
3474 this.StartUserStream();
3478 private class TwitterUserstream : IDisposable
3480 public event Action<string> StatusArrived;
3481 public event Action Stopped;
3482 public event Action Started;
3483 private HttpTwitter twCon;
3485 private Thread _streamThread;
3486 private bool _streamActive;
3488 private bool _allAtreplies = false;
3489 private string _trackwords = "";
3491 public TwitterUserstream(HttpTwitter twitterConnection)
3493 twCon = (HttpTwitter)twitterConnection.Clone();
3496 public void Start(bool allAtReplies, string trackwords)
3498 this.AllAtReplies = allAtReplies;
3499 this.TrackWords = trackwords;
3500 _streamActive = true;
3501 if (_streamThread != null && _streamThread.IsAlive) return;
3502 _streamThread = new Thread(UserStreamLoop);
3503 _streamThread.Name = "UserStreamReceiver";
3504 _streamThread.IsBackground = true;
3505 _streamThread.Start();
3512 return _streamActive;
3516 public bool AllAtReplies
3520 return _allAtreplies;
3524 _allAtreplies = value;
3528 public string TrackWords
3536 _trackwords = value;
3540 private void UserStreamLoop()
3546 StreamReader sr = null;
3549 if (!MyCommon.IsNetworkAvailable())
3557 var res = twCon.UserStream(ref st, _allAtreplies, _trackwords, Networking.GetUserAgentString());
3561 case HttpStatusCode.OK:
3562 Twitter.AccountState = MyCommon.ACCOUNT_STATE.Valid;
3564 case HttpStatusCode.Unauthorized:
3565 Twitter.AccountState = MyCommon.ACCOUNT_STATE.Invalid;
3573 //MyCommon.TraceOut("Stop:stream is null")
3577 sr = new StreamReader(st);
3579 while (_streamActive && !sr.EndOfStream && Twitter.AccountState == MyCommon.ACCOUNT_STATE.Valid)
3581 StatusArrived?.Invoke(sr.ReadLine());
3582 //this.LastTime = Now;
3585 if (sr.EndOfStream || Twitter.AccountState == MyCommon.ACCOUNT_STATE.Invalid)
3588 //MyCommon.TraceOut("Stop:EndOfStream")
3593 catch(WebException ex)
3595 if (ex.Status == WebExceptionStatus.Timeout)
3597 sleepSec = 30; //MyCommon.TraceOut("Stop:Timeout")
3599 else if (ex.Response != null && (int)((HttpWebResponse)ex.Response).StatusCode == 420)
3601 //MyCommon.TraceOut("Stop:Connection Limit")
3607 //MyCommon.TraceOut("Stop:WebException " + ex.Status.ToString())
3610 catch(ThreadAbortException)
3617 //MyCommon.TraceOut("Stop:IOException with Active." + Environment.NewLine + ex.Message)
3619 catch(ArgumentException ex)
3621 //System.ArgumentException: ストリームを読み取れませんでした。
3622 //サーバー側もしくは通信経路上で切断された場合?タイムアウト頻発後発生
3624 MyCommon.TraceOut(ex, "Stop:ArgumentException");
3628 MyCommon.TraceOut("Stop:Exception." + Environment.NewLine + ex.Message);
3629 MyCommon.ExceptionOut(ex);
3638 twCon.RequestAbort();
3643 while (_streamActive && ms < sleepSec * 1000)
3651 } while (this._streamActive);
3657 MyCommon.TraceOut("Stop:EndLoop");
3660 #region "IDisposable Support"
3661 private bool disposedValue; // 重複する呼び出しを検出するには
3664 protected virtual void Dispose(bool disposing)
3666 if (!this.disposedValue)
3670 _streamActive = false;
3671 if (_streamThread != null && _streamThread.IsAlive)
3673 _streamThread.Abort();
3677 this.disposedValue = true;
3680 //protected Overrides void Finalize()
3682 // // このコードを変更しないでください。クリーンアップ コードを上の Dispose(bool disposing) に記述します。
3684 // MyBase.Finalize()
3687 // このコードは、破棄可能なパターンを正しく実装できるように Visual Basic によって追加されました。
3688 public void Dispose()
3690 // このコードを変更しないでください。クリーンアップ コードを上の Dispose(bool disposing) に記述します。
3692 GC.SuppressFinalize(this);
3699 #region "IDisposable Support"
3700 private bool disposedValue; // 重複する呼び出しを検出するには
3703 protected virtual void Dispose(bool disposing)
3705 if (!this.disposedValue)
3709 this.StopUserStream();
3712 this.disposedValue = true;
3715 //protected Overrides void Finalize()
3717 // // このコードを変更しないでください。クリーンアップ コードを上の Dispose(bool disposing) に記述します。
3719 // MyBase.Finalize()
3722 // このコードは、破棄可能なパターンを正しく実装できるように Visual Basic によって追加されました。
3723 public void Dispose()
3725 // このコードを変更しないでください。クリーンアップ コードを上の Dispose(bool disposing) に記述します。
3727 GC.SuppressFinalize(this);
3732 public class PostDeletedEventArgs : EventArgs
3734 public long StatusId { get; }
3736 public PostDeletedEventArgs(long statusId)
3738 this.StatusId = statusId;
3742 public class UserStreamEventReceivedEventArgs : EventArgs
3744 public Twitter.FormattedEvent EventData { get; }
3746 public UserStreamEventReceivedEventArgs(Twitter.FormattedEvent eventData)
3748 this.EventData = eventData;