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 var quoteStatusIds = GetQuoteTweetStatusIds(entities);
1631 if (post.InReplyToStatusId != null)
1632 quoteStatusIds = quoteStatusIds.Concat(new[] { post.InReplyToStatusId.Value });
1634 post.QuoteStatusIds = quoteStatusIds
1635 .Where(x => x != post.StatusId && x != post.RetweetedId)
1636 .Distinct().ToArray();
1638 post.ExpandedUrls = entities.OfType<TwitterEntityUrl>()
1639 .Select(x => new PostClass.ExpandedUrlInfo(x.Url, x.ExpandedUrl))
1643 var source = ParseSource(sourceHtml);
1644 post.Source = source.Item1;
1645 post.SourceUri = source.Item2;
1647 post.IsReply = post.ReplyToList.Contains(_uname);
1648 post.IsExcludeReply = false;
1656 if (followerId.Count > 0) post.IsOwl = !followerId.Contains(post.UserId);
1664 /// ツイートに含まれる引用ツイートのURLからステータスIDを抽出
1666 public static IEnumerable<long> GetQuoteTweetStatusIds(IEnumerable<TwitterEntity> entities)
1668 var urls = entities.OfType<TwitterEntityUrl>().Select(x => x.ExpandedUrl);
1670 return GetQuoteTweetStatusIds(urls);
1673 public static IEnumerable<long> GetQuoteTweetStatusIds(IEnumerable<string> urls)
1675 foreach (var url in urls)
1677 var match = Twitter.StatusUrlRegex.Match(url);
1681 if (long.TryParse(match.Groups["StatusId"].Value, out statusId))
1682 yield return statusId;
1687 private long? CreatePostsFromJson(string content, MyCommon.WORKERTYPE gType, TabClass tab, bool read)
1689 TwitterStatus[] items;
1692 items = TwitterStatus.ParseJsonArray(content);
1694 catch(SerializationException ex)
1696 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
1697 throw new WebApiException("Json Parse Error(DataContractJsonSerializer)", content, ex);
1701 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
1702 throw new WebApiException("Invalid Json!", content, ex);
1705 long? minimumId = null;
1707 foreach (var status in items)
1709 PostClass post = null;
1710 post = CreatePostsFromStatusData(status);
1711 if (post == null) continue;
1713 if (minimumId == null || minimumId.Value > post.StatusId)
1714 minimumId = post.StatusId;
1721 if (TabInformations.GetInstance().ContainsKey(post.StatusId)) continue;
1725 if (tab.Contains(post.StatusId)) continue;
1730 if (gType != MyCommon.WORKERTYPE.UserTimeline &&
1731 post.RetweetedByUserId != null && this.noRTId.Contains(post.RetweetedByUserId.Value)) continue;
1734 if (post.IsMe && !read && _readOwnPost) post.IsRead = true;
1736 //非同期アイコン取得&StatusDictionaryに追加
1737 if (tab != null && tab.IsInnerStorageTabType)
1738 tab.AddPostToInnerStorage(post);
1740 TabInformations.GetInstance().AddPost(post);
1746 private long? CreatePostsFromSearchJson(string content, TabClass tab, bool read, int count, bool more)
1748 TwitterSearchResult items;
1751 items = TwitterSearchResult.ParseJson(content);
1753 catch (SerializationException ex)
1755 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
1756 throw new WebApiException("Json Parse Error(DataContractJsonSerializer)", content, ex);
1758 catch (Exception ex)
1760 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
1761 throw new WebApiException("Invalid Json!", content, ex);
1764 long? minimumId = null;
1766 foreach (var result in items.Statuses)
1768 PostClass post = null;
1769 post = CreatePostsFromStatusData(result);
1773 // Search API は相変わらずぶっ壊れたデータを返すことがあるため、必要なデータが欠如しているものは取得し直す
1776 post = this.GetStatusApi(read, result.Id);
1778 catch (WebApiException)
1784 if (minimumId == null || minimumId.Value > post.StatusId)
1785 minimumId = post.StatusId;
1787 if (!more && post.StatusId > tab.SinceId) tab.SinceId = post.StatusId;
1793 if (TabInformations.GetInstance().ContainsKey(post.StatusId)) continue;
1797 if (tab.Contains(post.StatusId)) continue;
1802 if ((post.IsMe && !read) && this._readOwnPost) post.IsRead = true;
1804 //非同期アイコン取得&StatusDictionaryに追加
1805 if (tab != null && tab.IsInnerStorageTabType)
1806 tab.AddPostToInnerStorage(post);
1808 TabInformations.GetInstance().AddPost(post);
1814 private void CreateFavoritePostsFromJson(string content, bool read)
1816 TwitterStatus[] item;
1819 item = TwitterStatus.ParseJsonArray(content);
1821 catch (SerializationException ex)
1823 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
1824 throw new WebApiException("Json Parse Error(DataContractJsonSerializer)", content, ex);
1826 catch (Exception ex)
1828 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
1829 throw new WebApiException("Invalid Json!", content, ex);
1832 var favTab = TabInformations.GetInstance().GetTabByType(MyCommon.TabUsageType.Favorites);
1834 foreach (var status in item)
1839 if (favTab.Contains(status.Id)) continue;
1842 var post = CreatePostsFromStatusData(status, true);
1843 if (post == null) continue;
1847 TabInformations.GetInstance().AddPost(post);
1851 public void GetListStatus(bool read,
1858 var count = GetApiResultCount(MyCommon.WORKERTYPE.List, more, startup);
1864 res = twCon.GetListsStatuses(tab.ListInfo.UserId, tab.ListInfo.Id, count, tab.OldestId, null, SettingCommon.Instance.IsListsIncludeRts, ref content);
1868 res = twCon.GetListsStatuses(tab.ListInfo.UserId, tab.ListInfo.Id, count, null, null, SettingCommon.Instance.IsListsIncludeRts, ref content);
1873 throw new WebApiException("Err:" + ex.Message, ex);
1876 this.CheckStatusCode(res, content);
1878 var minimumId = CreatePostsFromJson(content, MyCommon.WORKERTYPE.List, tab, read);
1880 if (minimumId != null)
1881 tab.OldestId = minimumId.Value;
1885 /// startStatusId からリプライ先の発言を辿る。発言は posts 以外からは検索しない。
1887 /// <returns>posts の中から検索されたリプライチェインの末端</returns>
1888 internal static PostClass FindTopOfReplyChain(IDictionary<Int64, PostClass> posts, Int64 startStatusId)
1890 if (!posts.ContainsKey(startStatusId))
1891 throw new ArgumentException("startStatusId (" + startStatusId + ") が posts の中から見つかりませんでした。", nameof(startStatusId));
1893 var nextPost = posts[startStatusId];
1894 while (nextPost.InReplyToStatusId != null)
1896 if (!posts.ContainsKey(nextPost.InReplyToStatusId.Value))
1898 nextPost = posts[nextPost.InReplyToStatusId.Value];
1904 public void GetRelatedResult(bool read, TabClass tab)
1906 var relPosts = new Dictionary<Int64, PostClass>();
1907 if (tab.RelationTargetPost.TextFromApi.Contains("@") && tab.RelationTargetPost.InReplyToStatusId == null)
1910 var p = TabInformations.GetInstance()[tab.RelationTargetPost.StatusId];
1911 if (p != null && p.InReplyToStatusId != null)
1913 tab.RelationTargetPost = p;
1917 p = this.GetStatusApi(read, tab.RelationTargetPost.StatusId);
1918 tab.RelationTargetPost = p;
1921 relPosts.Add(tab.RelationTargetPost.StatusId, tab.RelationTargetPost);
1923 Exception lastException = null;
1925 // in_reply_to_status_id を使用してリプライチェインを辿る
1926 var nextPost = FindTopOfReplyChain(relPosts, tab.RelationTargetPost.StatusId);
1928 while (nextPost.InReplyToStatusId != null && loopCount++ <= 20)
1930 var inReplyToId = nextPost.InReplyToStatusId.Value;
1932 var inReplyToPost = TabInformations.GetInstance()[inReplyToId];
1933 if (inReplyToPost == null)
1937 inReplyToPost = this.GetStatusApi(read, inReplyToId);
1939 catch (WebApiException ex)
1946 relPosts.Add(inReplyToPost.StatusId, inReplyToPost);
1948 nextPost = FindTopOfReplyChain(relPosts, nextPost.StatusId);
1951 //MRTとかに対応のためツイート内にあるツイートを指すURLを取り込む
1952 var text = tab.RelationTargetPost.Text;
1953 var ma = Twitter.StatusUrlRegex.Matches(text).Cast<Match>()
1954 .Concat(Twitter.ThirdPartyStatusUrlRegex.Matches(text).Cast<Match>());
1955 foreach (var _match in ma)
1958 if (Int64.TryParse(_match.Groups["StatusId"].Value, out _statusId))
1960 if (relPosts.ContainsKey(_statusId))
1963 var p = TabInformations.GetInstance()[_statusId];
1968 p = this.GetStatusApi(read, _statusId);
1970 catch (WebApiException ex)
1978 relPosts.Add(p.StatusId, p);
1982 relPosts.Values.ToList().ForEach(p =>
1984 if (p.IsMe && !read && this._readOwnPost)
1989 tab.AddPostToInnerStorage(p);
1992 if (lastException != null)
1993 throw new WebApiException(lastException.Message, lastException);
1996 public void GetSearch(bool read,
2002 var count = GetApiResultCount(MyCommon.WORKERTYPE.PublicSearch, more, false);
2004 long? sinceId = null;
2007 maxId = tab.OldestId - 1;
2011 sinceId = tab.SinceId;
2016 // TODO:一時的に40>100件に 件数変更UI作成の必要あり
2017 res = twCon.Search(tab.SearchWords, tab.SearchLang, count, maxId, sinceId, ref content);
2021 throw new WebApiException("Err:" + ex.Message, ex);
2025 case HttpStatusCode.BadRequest:
2026 throw new WebApiException("Invalid query", content);
2027 case HttpStatusCode.NotFound:
2028 throw new WebApiException("Invalid query", content);
2029 case HttpStatusCode.PaymentRequired: //API Documentには420と書いてあるが、該当コードがないので402にしてある
2030 throw new WebApiException("Search API Limit?", content);
2031 case HttpStatusCode.OK:
2034 throw new WebApiException("Err:" + res.ToString() + "(" + MethodBase.GetCurrentMethod().Name + ")", content);
2037 if (!TabInformations.GetInstance().ContainsTab(tab))
2040 var minimumId = this.CreatePostsFromSearchJson(content, tab, read, count, more);
2042 if (minimumId != null)
2043 tab.OldestId = minimumId.Value;
2046 private void CreateDirectMessagesFromJson(string content, MyCommon.WORKERTYPE gType, bool read)
2048 TwitterDirectMessage[] item;
2051 if (gType == MyCommon.WORKERTYPE.UserStream)
2053 item = new[] { TwitterStreamEventDirectMessage.ParseJson(content).DirectMessage };
2057 item = TwitterDirectMessage.ParseJsonArray(content);
2060 catch(SerializationException ex)
2062 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
2063 throw new WebApiException("Json Parse Error(DataContractJsonSerializer)", content, ex);
2067 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
2068 throw new WebApiException("Invalid Json!", content, ex);
2071 foreach (var message in item)
2073 var post = new PostClass();
2076 post.StatusId = message.Id;
2077 if (gType != MyCommon.WORKERTYPE.UserStream)
2079 if (gType == MyCommon.WORKERTYPE.DirectMessegeRcv)
2081 if (minDirectmessage > post.StatusId) minDirectmessage = post.StatusId;
2085 if (minDirectmessageSent > post.StatusId) minDirectmessageSent = post.StatusId;
2092 if (TabInformations.GetInstance().GetTabByType(MyCommon.TabUsageType.DirectMessage).Contains(post.StatusId)) continue;
2096 post.CreatedAt = MyCommon.DateTimeParse(message.CreatedAt);
2098 var textFromApi = message.Text;
2100 post.Text = CreateHtmlAnchor(textFromApi, post.ReplyToList, message.Entities, post.Media);
2101 post.TextFromApi = this.ReplaceTextFromApi(textFromApi, message.Entities);
2102 post.TextFromApi = WebUtility.HtmlDecode(post.TextFromApi);
2103 post.TextFromApi = post.TextFromApi.Replace("<3", "\u2661");
2106 post.QuoteStatusIds = GetQuoteTweetStatusIds(message.Entities).Distinct().ToArray();
2108 post.ExpandedUrls = message.Entities.OfType<TwitterEntityUrl>()
2109 .Select(x => new PostClass.ExpandedUrlInfo(x.Url, x.ExpandedUrl))
2114 if (gType == MyCommon.WORKERTYPE.UserStream)
2116 if (twCon.AuthenticatedUsername.Equals(message.Recipient.ScreenName, StringComparison.CurrentCultureIgnoreCase))
2118 user = message.Sender;
2124 user = message.Recipient;
2131 if (gType == MyCommon.WORKERTYPE.DirectMessegeRcv)
2133 user = message.Sender;
2139 user = message.Recipient;
2145 post.UserId = user.Id;
2146 post.ScreenName = user.ScreenName;
2147 post.Nickname = user.Name.Trim();
2148 post.ImageUrl = user.ProfileImageUrlHttps;
2149 post.IsProtect = user.Protected;
2153 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
2154 MessageBox.Show("Parse Error(CreateDirectMessagesFromJson)");
2159 if (post.IsMe && !read && _readOwnPost) post.IsRead = true;
2160 post.IsReply = false;
2161 post.IsExcludeReply = false;
2164 var dmTab = TabInformations.GetInstance().GetTabByType(MyCommon.TabUsageType.DirectMessage);
2165 dmTab.AddPostToInnerStorage(post);
2169 public void GetDirectMessageApi(bool read,
2170 MyCommon.WORKERTYPE gType,
2173 this.CheckAccountState();
2174 this.CheckAccessLevel(TwitterApiAccessLevel.ReadWriteAndDirectMessage);
2178 var count = GetApiResultCount(gType, more, false);
2182 if (gType == MyCommon.WORKERTYPE.DirectMessegeRcv)
2186 res = twCon.DirectMessages(count, minDirectmessage, null, ref content);
2190 res = twCon.DirectMessages(count, null, null, ref content);
2197 res = twCon.DirectMessagesSent(count, minDirectmessageSent, null, ref content);
2201 res = twCon.DirectMessagesSent(count, null, null, ref content);
2207 throw new WebApiException("Err:" + ex.Message, ex);
2210 this.CheckStatusCode(res, content);
2212 CreateDirectMessagesFromJson(content, gType, read);
2215 public void GetFavoritesApi(bool read,
2218 this.CheckAccountState();
2222 var count = GetApiResultCount(MyCommon.WORKERTYPE.Favorites, more, false);
2226 res = twCon.Favorites(count, ref content);
2230 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
2233 this.CheckStatusCode(res, content);
2235 CreateFavoritePostsFromJson(content, read);
2238 private string ReplaceTextFromApi(string text, TwitterEntities entities)
2240 if (entities != null)
2242 if (entities.Urls != null)
2244 foreach (var m in entities.Urls)
2246 if (!string.IsNullOrEmpty(m.DisplayUrl)) text = text.Replace(m.Url, m.DisplayUrl);
2249 if (entities.Media != null)
2251 foreach (var m in entities.Media)
2253 if (!string.IsNullOrEmpty(m.DisplayUrl)) text = text.Replace(m.Url, m.DisplayUrl);
2263 /// <exception cref="WebApiException"/>
2264 public void RefreshFollowerIds()
2266 if (MyCommon._endingFlag) return;
2269 var newFollowerIds = new HashSet<long>();
2272 var ret = this.GetFollowerIdsApi(ref cursor);
2273 newFollowerIds.UnionWith(ret.Ids);
2274 cursor = ret.NextCursor;
2275 } while (cursor != 0);
2277 this.followerId = newFollowerIds;
2278 TabInformations.GetInstance().RefreshOwl(this.followerId);
2280 this._GetFollowerResult = true;
2283 public bool GetFollowersSuccess
2287 return _GetFollowerResult;
2291 private TwitterIds GetFollowerIdsApi(ref long cursor)
2293 this.CheckAccountState();
2299 res = twCon.FollowerIds(cursor, ref content);
2303 throw new WebApiException("Err:" + e.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", e);
2306 this.CheckStatusCode(res, content);
2310 var ret = TwitterIds.ParseJson(content);
2312 if (ret.Ids == null)
2314 var ex = new WebApiException("Err: ret.id == null (GetFollowerIdsApi)", content);
2315 MyCommon.ExceptionOut(ex);
2321 catch(SerializationException e)
2323 var ex = new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, e);
2324 MyCommon.TraceOut(ex);
2329 var ex = new WebApiException("Err:Invalid Json!", content, e);
2330 MyCommon.TraceOut(ex);
2336 /// RT 非表示ユーザーを更新します
2338 /// <exception cref="WebApiException"/>
2339 public void RefreshNoRetweetIds()
2341 if (MyCommon._endingFlag) return;
2343 this.noRTId = this.NoRetweetIdsApi();
2345 this._GetNoRetweetResult = true;
2348 private long[] NoRetweetIdsApi()
2350 this.CheckAccountState();
2356 res = twCon.NoRetweetIds(ref content);
2360 throw new WebApiException("Err:" + e.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", e);
2363 this.CheckStatusCode(res, content);
2367 return MyCommon.CreateDataFromJson<long[]>(content);
2369 catch(SerializationException e)
2371 var ex = new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, e);
2372 MyCommon.TraceOut(ex);
2377 var ex = new WebApiException("Err:Invalid Json!", content, e);
2378 MyCommon.TraceOut(ex);
2383 public bool GetNoRetweetSuccess
2387 return _GetNoRetweetResult;
2392 /// t.co の文字列長などの設定情報を更新します
2394 /// <exception cref="WebApiException"/>
2395 public void RefreshConfiguration()
2397 this.Configuration = this.ConfigurationApi();
2400 private TwitterConfiguration ConfigurationApi()
2406 res = twCon.GetConfiguration(ref content);
2410 throw new WebApiException("Err:" + e.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", e);
2413 this.CheckStatusCode(res, content);
2417 return TwitterConfiguration.ParseJson(content);
2419 catch(SerializationException e)
2421 var ex = new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, e);
2422 MyCommon.TraceOut(ex);
2427 var ex = new WebApiException("Err:Invalid Json!", content, e);
2428 MyCommon.TraceOut(ex);
2433 public void GetListsApi()
2435 this.CheckAccountState();
2438 IEnumerable<ListElement> lists;
2443 res = twCon.GetLists(this.Username, ref content);
2445 catch (Exception ex)
2447 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
2450 this.CheckStatusCode(res, content);
2454 lists = TwitterList.ParseJsonArray(content)
2455 .Select(x => new ListElement(x, this));
2457 catch (SerializationException ex)
2459 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
2460 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
2462 catch (Exception ex)
2464 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
2465 throw new WebApiException("Err:Invalid Json!", content, ex);
2470 res = twCon.GetListsSubscriptions(this.Username, ref content);
2472 catch (Exception ex)
2474 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
2477 this.CheckStatusCode(res, content);
2481 lists = lists.Concat(TwitterList.ParseJsonArray(content)
2482 .Select(x => new ListElement(x, this)));
2484 catch (SerializationException ex)
2486 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
2487 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
2489 catch (Exception ex)
2491 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
2492 throw new WebApiException("Err:Invalid Json!", content, ex);
2495 TabInformations.GetInstance().SubscribableLists = lists.ToList();
2498 public void DeleteList(string list_id)
2505 res = twCon.DeleteListID(this.Username, list_id, ref content);
2509 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
2512 this.CheckStatusCode(res, content);
2515 public ListElement EditList(string list_id, string new_name, bool isPrivate, string description)
2522 res = twCon.UpdateListID(this.Username, list_id, new_name, isPrivate, description, ref content);
2526 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
2529 this.CheckStatusCode(res, content);
2533 var le = TwitterList.ParseJson(content);
2534 return new ListElement(le, this);
2536 catch(SerializationException ex)
2538 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
2539 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
2543 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
2544 throw new WebApiException("Err:Invalid Json!", content, ex);
2548 public long GetListMembers(string list_id, List<UserInfo> lists, long cursor)
2550 this.CheckAccountState();
2556 res = twCon.GetListMembers(this.Username, list_id, cursor, ref content);
2560 throw new WebApiException("Err:" + ex.Message);
2563 this.CheckStatusCode(res, content);
2567 var users = TwitterUsers.ParseJson(content);
2568 Array.ForEach<TwitterUser>(
2570 u => lists.Add(new UserInfo(u)));
2572 return users.NextCursor;
2574 catch(SerializationException ex)
2576 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
2577 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
2581 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
2582 throw new WebApiException("Err:Invalid Json!", content, ex);
2586 public void CreateListApi(string listName, bool isPrivate, string description)
2588 this.CheckAccountState();
2594 res = twCon.CreateLists(listName, isPrivate, description, ref content);
2598 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
2601 this.CheckStatusCode(res, content);
2605 var le = TwitterList.ParseJson(content);
2606 TabInformations.GetInstance().SubscribableLists.Add(new ListElement(le, this));
2608 catch(SerializationException ex)
2610 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
2611 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
2615 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
2616 throw new WebApiException("Err:Invalid Json!", content, ex);
2620 public bool ContainsUserAtList(string listId, string user)
2622 this.CheckAccountState();
2629 res = this.twCon.ShowListMember(listId, user, ref content);
2633 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
2636 if (res == HttpStatusCode.NotFound)
2641 this.CheckStatusCode(res, content);
2645 TwitterUser.ParseJson(content);
2654 public void AddUserToList(string listId, string user)
2661 res = twCon.CreateListMembers(listId, user, ref content);
2665 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
2668 this.CheckStatusCode(res, content);
2671 public void RemoveUserToList(string listId, string user)
2678 res = twCon.DeleteListMembers(listId, user, ref content);
2682 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
2685 this.CheckStatusCode(res, content);
2688 public string CreateHtmlAnchor(string text, List<string> AtList, TwitterEntities entities, List<MediaInfo> media)
2690 if (entities != null)
2692 if (entities.Hashtags != null)
2696 this._hashList.AddRange(entities.Hashtags.Select(x => "#" + x.Text));
2699 if (entities.UserMentions != null)
2701 foreach (var ent in entities.UserMentions)
2703 var screenName = ent.ScreenName.ToLowerInvariant();
2704 if (!AtList.Contains(screenName))
2705 AtList.Add(screenName);
2708 if (entities.Media != null)
2712 foreach (var ent in entities.Media)
2714 if (!media.Any(x => x.Url == ent.MediaUrl))
2716 if (ent.VideoInfo != null &&
2717 ent.Type == "animated_gif" || ent.Type == "video")
2719 //var videoUrl = ent.VideoInfo.Variants
2720 // .Where(v => v.ContentType == "video/mp4")
2721 // .OrderByDescending(v => v.Bitrate)
2722 // .Select(v => v.Url).FirstOrDefault();
2723 media.Add(new MediaInfo(ent.MediaUrl, ent.ExpandedUrl));
2726 media.Add(new MediaInfo(ent.MediaUrl));
2733 // PostClass.ExpandedUrlInfo を使用して非同期に URL 展開を行うためここでは expanded_url を使用しない
2734 text = TweetFormatter.AutoLinkHtml(text, entities, keepTco: true);
2736 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>");
2737 text = PreProcessUrl(text); //IDN置換
2742 private static readonly Uri SourceUriBase = new Uri("https://twitter.com/");
2745 /// Twitter APIから得たHTML形式のsource文字列を分析し、source名とURLに分離します
2747 public static Tuple<string, Uri> ParseSource(string sourceHtml)
2749 if (string.IsNullOrEmpty(sourceHtml))
2750 return Tuple.Create<string, Uri>("", null);
2755 // sourceHtmlの例: <a href="http://twitter.com" rel="nofollow">Twitter Web Client</a>
2757 var match = Regex.Match(sourceHtml, "^<a href=\"(?<uri>.+?)\".*?>(?<text>.+)</a>$", RegexOptions.IgnoreCase);
2760 sourceText = WebUtility.HtmlDecode(match.Groups["text"].Value);
2763 var uriStr = WebUtility.HtmlDecode(match.Groups["uri"].Value);
2764 sourceUri = new Uri(SourceUriBase, uriStr);
2766 catch (UriFormatException)
2773 sourceText = WebUtility.HtmlDecode(sourceHtml);
2777 return Tuple.Create(sourceText, sourceUri);
2780 public TwitterApiStatus GetInfoApi()
2782 if (Twitter.AccountState != MyCommon.ACCOUNT_STATE.Valid) return null;
2784 if (MyCommon._endingFlag) return null;
2790 res = twCon.RateLimitStatus(ref content);
2794 this.ResetApiStatus();
2798 this.CheckStatusCode(res, content);
2802 MyCommon.TwitterApiInfo.UpdateFromJson(content);
2803 return MyCommon.TwitterApiInfo;
2805 catch (Exception ex)
2807 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
2808 MyCommon.TwitterApiInfo.Reset();
2814 /// ブロック中のユーザーを更新します
2816 /// <exception cref="WebApiException"/>
2817 public void RefreshBlockIds()
2819 if (MyCommon._endingFlag) return;
2822 var newBlockIds = new HashSet<long>();
2825 var ret = this.GetBlockIdsApi(cursor);
2826 newBlockIds.UnionWith(ret.Ids);
2827 cursor = ret.NextCursor;
2828 } while (cursor != 0);
2830 newBlockIds.Remove(this.UserId); // 元のソースにあったので一応残しておく
2832 TabInformations.GetInstance().BlockIds = newBlockIds;
2835 public TwitterIds GetBlockIdsApi(long cursor)
2837 this.CheckAccountState();
2843 res = twCon.GetBlockUserIds(ref content, cursor);
2847 throw new WebApiException("Err:" + e.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", e);
2850 this.CheckStatusCode(res, content);
2854 return TwitterIds.ParseJson(content);
2856 catch(SerializationException e)
2858 var ex = new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, e);
2859 MyCommon.TraceOut(ex);
2864 var ex = new WebApiException("Err:Invalid Json!", content, e);
2865 MyCommon.TraceOut(ex);
2871 /// ミュート中のユーザーIDを更新します
2873 /// <exception cref="WebApiException"/>
2874 public async Task RefreshMuteUserIdsAsync()
2876 if (MyCommon._endingFlag) return;
2878 var ids = await TwitterIds.GetAllItemsAsync(this.GetMuteUserIdsApiAsync)
2879 .ConfigureAwait(false);
2881 TabInformations.GetInstance().MuteUserIds = new HashSet<long>(ids);
2884 public async Task<TwitterIds> GetMuteUserIdsApiAsync(long cursor)
2890 var res = await Task.Run(() => twCon.GetMuteUserIds(ref content, cursor))
2891 .ConfigureAwait(false);
2893 this.CheckStatusCode(res, content);
2895 return TwitterIds.ParseJson(content);
2897 catch (WebException ex)
2899 var ex2 = new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", content, ex);
2900 MyCommon.TraceOut(ex2);
2903 catch (SerializationException ex)
2905 var ex2 = new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
2906 MyCommon.TraceOut(ex2);
2911 public string[] GetHashList()
2916 hashArray = _hashList.ToArray();
2922 public string AccessToken
2926 return twCon.AccessToken;
2930 public string AccessTokenSecret
2934 return twCon.AccessTokenSecret;
2938 private void CheckAccountState()
2940 if (Twitter.AccountState != MyCommon.ACCOUNT_STATE.Valid)
2941 throw new WebApiException("Auth error. Check your account");
2944 private void CheckAccessLevel(TwitterApiAccessLevel accessLevelFlags)
2946 if (!this.AccessLevel.HasFlag(accessLevelFlags))
2947 throw new WebApiException("Auth Err:try to re-authorization.");
2950 private void CheckStatusCode(HttpStatusCode httpStatus, string responseText,
2951 [CallerMemberName] string callerMethodName = "")
2953 if (httpStatus == HttpStatusCode.OK)
2955 Twitter.AccountState = MyCommon.ACCOUNT_STATE.Valid;
2959 if (string.IsNullOrWhiteSpace(responseText))
2961 if (httpStatus == HttpStatusCode.Unauthorized)
2962 Twitter.AccountState = MyCommon.ACCOUNT_STATE.Invalid;
2964 throw new WebApiException("Err:" + httpStatus + "(" + callerMethodName + ")");
2969 var errors = TwitterError.ParseJson(responseText).Errors;
2970 if (errors == null || !errors.Any())
2972 throw new WebApiException("Err:" + httpStatus + "(" + callerMethodName + ")", responseText);
2975 foreach (var error in errors)
2977 if (error.Code == TwitterErrorCode.InvalidToken ||
2978 error.Code == TwitterErrorCode.SuspendedAccount)
2980 Twitter.AccountState = MyCommon.ACCOUNT_STATE.Invalid;
2984 throw new WebApiException("Err:" + string.Join(",", errors.Select(x => x.ToString())) + "(" + callerMethodName + ")", responseText);
2986 catch (SerializationException) { }
2988 throw new WebApiException("Err:" + httpStatus + "(" + callerMethodName + ")", responseText);
2991 public int GetTextLengthRemain(string postText)
2993 var matchDm = Twitter.DMSendTextRegex.Match(postText);
2994 if (matchDm.Success)
2995 return this.GetTextLengthRemainInternal(matchDm.Groups["body"].Value, isDm: true);
2997 return this.GetTextLengthRemainInternal(postText, isDm: false);
3000 private int GetTextLengthRemainInternal(string postText, bool isDm)
3005 while (pos < postText.Length)
3009 if (char.IsSurrogatePair(postText, pos))
3010 pos += 2; // サロゲートペアの場合は2文字分進める
3015 var urls = TweetExtractor.ExtractUrls(postText);
3016 foreach (var url in urls)
3018 var shortUrlLength = url.StartsWith("https://", StringComparison.OrdinalIgnoreCase)
3019 ? this.Configuration.ShortUrlLengthHttps
3020 : this.Configuration.ShortUrlLength;
3022 textLength += shortUrlLength - url.Length;
3026 return this.Configuration.DmTextCharacterLimit - textLength;
3028 return 140 - textLength;
3032 #region "UserStream"
3033 private string trackWord_ = "";
3034 public string TrackWord
3045 private bool allAtReply_ = false;
3046 public bool AllAtReply
3054 allAtReply_ = value;
3058 public event EventHandler NewPostFromStream;
3059 public event EventHandler UserStreamStarted;
3060 public event EventHandler UserStreamStopped;
3061 public event EventHandler<PostDeletedEventArgs> PostDeleted;
3062 public event EventHandler<UserStreamEventReceivedEventArgs> UserStreamEventReceived;
3063 private DateTime _lastUserstreamDataReceived;
3064 private TwitterUserstream userStream;
3066 public class FormattedEvent
3068 public MyCommon.EVENTTYPE Eventtype { get; set; }
3069 public DateTime CreatedAt { get; set; }
3070 public string Event { get; set; }
3071 public string Username { get; set; }
3072 public string Target { get; set; }
3073 public Int64 Id { get; set; }
3074 public bool IsMe { get; set; }
3077 public List<FormattedEvent> storedEvent_ = new List<FormattedEvent>();
3078 public List<FormattedEvent> StoredEvent
3082 return storedEvent_;
3086 storedEvent_ = value;
3090 private readonly IReadOnlyDictionary<string, MyCommon.EVENTTYPE> eventTable = new Dictionary<string, MyCommon.EVENTTYPE>
3092 ["favorite"] = MyCommon.EVENTTYPE.Favorite,
3093 ["unfavorite"] = MyCommon.EVENTTYPE.Unfavorite,
3094 ["follow"] = MyCommon.EVENTTYPE.Follow,
3095 ["list_member_added"] = MyCommon.EVENTTYPE.ListMemberAdded,
3096 ["list_member_removed"] = MyCommon.EVENTTYPE.ListMemberRemoved,
3097 ["block"] = MyCommon.EVENTTYPE.Block,
3098 ["unblock"] = MyCommon.EVENTTYPE.Unblock,
3099 ["user_update"] = MyCommon.EVENTTYPE.UserUpdate,
3100 ["deleted"] = MyCommon.EVENTTYPE.Deleted,
3101 ["list_created"] = MyCommon.EVENTTYPE.ListCreated,
3102 ["list_destroyed"] = MyCommon.EVENTTYPE.ListDestroyed,
3103 ["list_updated"] = MyCommon.EVENTTYPE.ListUpdated,
3104 ["unfollow"] = MyCommon.EVENTTYPE.Unfollow,
3105 ["list_user_subscribed"] = MyCommon.EVENTTYPE.ListUserSubscribed,
3106 ["list_user_unsubscribed"] = MyCommon.EVENTTYPE.ListUserUnsubscribed,
3107 ["mute"] = MyCommon.EVENTTYPE.Mute,
3108 ["unmute"] = MyCommon.EVENTTYPE.Unmute,
3109 ["quoted_tweet"] = MyCommon.EVENTTYPE.QuotedTweet,
3112 public bool IsUserstreamDataReceived
3116 return DateTime.Now.Subtract(this._lastUserstreamDataReceived).TotalSeconds < 31;
3120 private void userStream_StatusArrived(string line)
3122 this._lastUserstreamDataReceived = DateTime.Now;
3123 if (string.IsNullOrEmpty(line)) return;
3125 if (line.First() != '{' || line.Last() != '}')
3127 MyCommon.TraceOut("Invalid JSON (StatusArrived):" + Environment.NewLine + line);
3135 using (var jsonReader = JsonReaderWriterFactory.CreateJsonReader(Encoding.UTF8.GetBytes(line), XmlDictionaryReaderQuotas.Max))
3137 var xElm = XElement.Load(jsonReader);
3138 if (xElm.Element("friends") != null)
3140 Debug.WriteLine("friends");
3143 else if (xElm.Element("delete") != null)
3145 Debug.WriteLine("delete");
3148 if ((idElm = xElm.Element("delete").Element("direct_message")?.Element("id")) != null)
3151 long.TryParse(idElm.Value, out id);
3153 this.PostDeleted?.Invoke(this, new PostDeletedEventArgs(id));
3155 else if ((idElm = xElm.Element("delete").Element("status")?.Element("id")) != null)
3158 long.TryParse(idElm.Value, out id);
3160 this.PostDeleted?.Invoke(this, new PostDeletedEventArgs(id));
3164 MyCommon.TraceOut("delete:" + line);
3167 for (int i = this.StoredEvent.Count - 1; i >= 0; i--)
3169 var sEvt = this.StoredEvent[i];
3170 if (sEvt.Id == id && (sEvt.Event == "favorite" || sEvt.Event == "unfavorite"))
3172 this.StoredEvent.RemoveAt(i);
3177 else if (xElm.Element("limit") != null)
3179 Debug.WriteLine(line);
3182 else if (xElm.Element("event") != null)
3184 Debug.WriteLine("event: " + xElm.Element("event").Value);
3185 CreateEventFromJson(line);
3188 else if (xElm.Element("direct_message") != null)
3190 Debug.WriteLine("direct_message");
3193 else if (xElm.Element("retweeted_status") != null)
3195 var sourceUserId = xElm.XPathSelectElement("/user/id_str").Value;
3196 var targetUserId = xElm.XPathSelectElement("/retweeted_status/user/id_str").Value;
3198 // 自分に関係しないリツイートの場合は無視する
3199 var selfUserId = this.UserId.ToString();
3200 if (sourceUserId == selfUserId || targetUserId == selfUserId)
3202 // 公式 RT をイベントとしても扱う
3203 var evt = CreateEventFromRetweet(xElm);
3206 this.StoredEvent.Insert(0, evt);
3208 this.UserStreamEventReceived?.Invoke(this, new UserStreamEventReceivedEventArgs(evt));
3212 // 従来通り公式 RT の表示も行うため return しない
3214 else if (xElm.Element("scrub_geo") != null)
3218 TabInformations.GetInstance().ScrubGeoReserve(long.Parse(xElm.Element("scrub_geo").Element("user_id").Value),
3219 long.Parse(xElm.Element("scrub_geo").Element("up_to_status_id").Value));
3223 MyCommon.TraceOut("scrub_geo:" + line);
3231 CreateDirectMessagesFromJson(line, MyCommon.WORKERTYPE.UserStream, false);
3235 CreatePostsFromJson("[" + line + "]", MyCommon.WORKERTYPE.Timeline, null, false);
3238 catch (WebApiException ex)
3240 MyCommon.TraceOut(ex);
3243 catch(NullReferenceException)
3245 MyCommon.TraceOut("NullRef StatusArrived: " + line);
3248 this.NewPostFromStream?.Invoke(this, EventArgs.Empty);
3252 /// UserStreamsから受信した公式RTをイベントに変換します
3254 private FormattedEvent CreateEventFromRetweet(XElement xElm)
3256 return new FormattedEvent
3258 Eventtype = MyCommon.EVENTTYPE.Retweet,
3260 CreatedAt = MyCommon.DateTimeParse(xElm.XPathSelectElement("/created_at").Value),
3261 IsMe = xElm.XPathSelectElement("/user/id_str").Value == this.UserId.ToString(),
3262 Username = xElm.XPathSelectElement("/user/screen_name").Value,
3263 Target = string.Format("@{0}:{1}", new[]
3265 xElm.XPathSelectElement("/retweeted_status/user/screen_name").Value,
3266 WebUtility.HtmlDecode(xElm.XPathSelectElement("/retweeted_status/text").Value),
3268 Id = long.Parse(xElm.XPathSelectElement("/retweeted_status/id_str").Value),
3272 private void CreateEventFromJson(string content)
3274 TwitterStreamEvent eventData = null;
3277 eventData = TwitterStreamEvent.ParseJson(content);
3279 catch(SerializationException ex)
3281 MyCommon.TraceOut(ex, "Event Serialize Exception!" + Environment.NewLine + content);
3285 MyCommon.TraceOut(ex, "Event Exception!" + Environment.NewLine + content);
3288 var evt = new FormattedEvent();
3289 evt.CreatedAt = MyCommon.DateTimeParse(eventData.CreatedAt);
3290 evt.Event = eventData.Event;
3291 evt.Username = eventData.Source.ScreenName;
3292 evt.IsMe = evt.Username.ToLowerInvariant().Equals(this.Username.ToLowerInvariant());
3294 MyCommon.EVENTTYPE eventType;
3295 eventTable.TryGetValue(eventData.Event, out eventType);
3296 evt.Eventtype = eventType;
3298 TwitterStreamEvent<TwitterStatus> tweetEvent;
3300 switch (eventData.Event)
3302 case "access_revoked":
3303 case "access_unrevoked":
3305 case "user_suspend":
3308 if (eventData.Target.ScreenName.ToLowerInvariant().Equals(_uname))
3310 if (!this.followerId.Contains(eventData.Source.Id)) this.followerId.Add(eventData.Source.Id);
3314 return; //Block後のUndoをすると、SourceとTargetが逆転したfollowイベントが帰ってくるため。
3319 evt.Target = "@" + eventData.Target.ScreenName;
3321 case "favorited_retweet":
3322 case "retweeted_retweet":
3326 tweetEvent = TwitterStreamEvent<TwitterStatus>.ParseJson(content);
3327 evt.Target = "@" + tweetEvent.TargetObject.User.ScreenName + ":" + WebUtility.HtmlDecode(tweetEvent.TargetObject.Text);
3328 evt.Id = tweetEvent.TargetObject.Id;
3330 if (SettingCommon.Instance.IsRemoveSameEvent)
3332 if (this.StoredEvent.Any(ev => ev.Username == evt.Username && ev.Eventtype == evt.Eventtype && ev.Target == evt.Target))
3336 var tabinfo = TabInformations.GetInstance();
3339 var statusId = tweetEvent.TargetObject.Id;
3340 if (!tabinfo.Posts.TryGetValue(statusId, out post))
3343 if (eventData.Event == "favorite")
3345 var favTab = tabinfo.GetTabByType(MyCommon.TabUsageType.Favorites);
3346 if (!favTab.Contains(post.StatusId))
3347 favTab.AddPostImmediately(post.StatusId, post.IsRead);
3349 if (tweetEvent.Source.Id == this.UserId)
3353 else if (tweetEvent.Target.Id == this.UserId)
3355 post.FavoritedCount++;
3357 if (SettingCommon.Instance.FavEventUnread)
3358 tabinfo.SetReadAllTab(post.StatusId, read: false);
3363 if (tweetEvent.Source.Id == this.UserId)
3367 else if (tweetEvent.Target.Id == this.UserId)
3369 post.FavoritedCount = Math.Max(0, post.FavoritedCount - 1);
3373 case "quoted_tweet":
3374 if (evt.IsMe) return;
3376 tweetEvent = TwitterStreamEvent<TwitterStatus>.ParseJson(content);
3377 evt.Target = "@" + tweetEvent.TargetObject.User.ScreenName + ":" + WebUtility.HtmlDecode(tweetEvent.TargetObject.Text);
3378 evt.Id = tweetEvent.TargetObject.Id;
3380 if (SettingCommon.Instance.IsRemoveSameEvent)
3382 if (this.StoredEvent.Any(ev => ev.Username == evt.Username && ev.Eventtype == evt.Eventtype && ev.Target == evt.Target))
3386 case "list_member_added":
3387 case "list_member_removed":
3388 case "list_created":
3389 case "list_destroyed":
3390 case "list_updated":
3391 case "list_user_subscribed":
3392 case "list_user_unsubscribed":
3393 var listEvent = TwitterStreamEvent<TwitterList>.ParseJson(content);
3394 evt.Target = listEvent.TargetObject.FullName;
3397 if (!TabInformations.GetInstance().BlockIds.Contains(eventData.Target.Id)) TabInformations.GetInstance().BlockIds.Add(eventData.Target.Id);
3401 if (TabInformations.GetInstance().BlockIds.Contains(eventData.Target.Id)) TabInformations.GetInstance().BlockIds.Remove(eventData.Target.Id);
3410 evt.Target = "@" + eventData.Target.ScreenName;
3411 if (!TabInformations.GetInstance().MuteUserIds.Contains(eventData.Target.Id))
3413 TabInformations.GetInstance().MuteUserIds.Add(eventData.Target.Id);
3417 evt.Target = "@" + eventData.Target.ScreenName;
3418 if (TabInformations.GetInstance().MuteUserIds.Contains(eventData.Target.Id))
3420 TabInformations.GetInstance().MuteUserIds.Remove(eventData.Target.Id);
3425 MyCommon.TraceOut("Unknown Event:" + evt.Event + Environment.NewLine + content);
3428 this.StoredEvent.Insert(0, evt);
3430 this.UserStreamEventReceived?.Invoke(this, new UserStreamEventReceivedEventArgs(evt));
3433 private void userStream_Started()
3435 this.UserStreamStarted?.Invoke(this, EventArgs.Empty);
3438 private void userStream_Stopped()
3440 this.UserStreamStopped?.Invoke(this, EventArgs.Empty);
3443 public bool UserStreamEnabled
3447 return userStream == null ? false : userStream.Enabled;
3451 public void StartUserStream()
3453 if (userStream != null)
3457 userStream = new TwitterUserstream(twCon);
3458 userStream.StatusArrived += userStream_StatusArrived;
3459 userStream.Started += userStream_Started;
3460 userStream.Stopped += userStream_Stopped;
3461 userStream.Start(this.AllAtReply, this.TrackWord);
3464 public void StopUserStream()
3466 userStream?.Dispose();
3468 if (!MyCommon._endingFlag)
3470 this.UserStreamStopped?.Invoke(this, EventArgs.Empty);
3474 public void ReconnectUserStream()
3476 if (userStream != null)
3478 this.StartUserStream();
3482 private class TwitterUserstream : IDisposable
3484 public event Action<string> StatusArrived;
3485 public event Action Stopped;
3486 public event Action Started;
3487 private HttpTwitter twCon;
3489 private Thread _streamThread;
3490 private bool _streamActive;
3492 private bool _allAtreplies = false;
3493 private string _trackwords = "";
3495 public TwitterUserstream(HttpTwitter twitterConnection)
3497 twCon = (HttpTwitter)twitterConnection.Clone();
3500 public void Start(bool allAtReplies, string trackwords)
3502 this.AllAtReplies = allAtReplies;
3503 this.TrackWords = trackwords;
3504 _streamActive = true;
3505 if (_streamThread != null && _streamThread.IsAlive) return;
3506 _streamThread = new Thread(UserStreamLoop);
3507 _streamThread.Name = "UserStreamReceiver";
3508 _streamThread.IsBackground = true;
3509 _streamThread.Start();
3516 return _streamActive;
3520 public bool AllAtReplies
3524 return _allAtreplies;
3528 _allAtreplies = value;
3532 public string TrackWords
3540 _trackwords = value;
3544 private void UserStreamLoop()
3550 StreamReader sr = null;
3553 if (!MyCommon.IsNetworkAvailable())
3561 var res = twCon.UserStream(ref st, _allAtreplies, _trackwords, Networking.GetUserAgentString());
3565 case HttpStatusCode.OK:
3566 Twitter.AccountState = MyCommon.ACCOUNT_STATE.Valid;
3568 case HttpStatusCode.Unauthorized:
3569 Twitter.AccountState = MyCommon.ACCOUNT_STATE.Invalid;
3577 //MyCommon.TraceOut("Stop:stream is null")
3581 sr = new StreamReader(st);
3583 while (_streamActive && !sr.EndOfStream && Twitter.AccountState == MyCommon.ACCOUNT_STATE.Valid)
3585 StatusArrived?.Invoke(sr.ReadLine());
3586 //this.LastTime = Now;
3589 if (sr.EndOfStream || Twitter.AccountState == MyCommon.ACCOUNT_STATE.Invalid)
3592 //MyCommon.TraceOut("Stop:EndOfStream")
3597 catch(WebException ex)
3599 if (ex.Status == WebExceptionStatus.Timeout)
3601 sleepSec = 30; //MyCommon.TraceOut("Stop:Timeout")
3603 else if (ex.Response != null && (int)((HttpWebResponse)ex.Response).StatusCode == 420)
3605 //MyCommon.TraceOut("Stop:Connection Limit")
3611 //MyCommon.TraceOut("Stop:WebException " + ex.Status.ToString())
3614 catch(ThreadAbortException)
3621 //MyCommon.TraceOut("Stop:IOException with Active." + Environment.NewLine + ex.Message)
3623 catch(ArgumentException ex)
3625 //System.ArgumentException: ストリームを読み取れませんでした。
3626 //サーバー側もしくは通信経路上で切断された場合?タイムアウト頻発後発生
3628 MyCommon.TraceOut(ex, "Stop:ArgumentException");
3632 MyCommon.TraceOut("Stop:Exception." + Environment.NewLine + ex.Message);
3633 MyCommon.ExceptionOut(ex);
3642 twCon.RequestAbort();
3647 while (_streamActive && ms < sleepSec * 1000)
3655 } while (this._streamActive);
3661 MyCommon.TraceOut("Stop:EndLoop");
3664 #region "IDisposable Support"
3665 private bool disposedValue; // 重複する呼び出しを検出するには
3668 protected virtual void Dispose(bool disposing)
3670 if (!this.disposedValue)
3674 _streamActive = false;
3675 if (_streamThread != null && _streamThread.IsAlive)
3677 _streamThread.Abort();
3681 this.disposedValue = true;
3684 //protected Overrides void Finalize()
3686 // // このコードを変更しないでください。クリーンアップ コードを上の Dispose(bool disposing) に記述します。
3688 // MyBase.Finalize()
3691 // このコードは、破棄可能なパターンを正しく実装できるように Visual Basic によって追加されました。
3692 public void Dispose()
3694 // このコードを変更しないでください。クリーンアップ コードを上の Dispose(bool disposing) に記述します。
3696 GC.SuppressFinalize(this);
3703 #region "IDisposable Support"
3704 private bool disposedValue; // 重複する呼び出しを検出するには
3707 protected virtual void Dispose(bool disposing)
3709 if (!this.disposedValue)
3713 this.StopUserStream();
3716 this.disposedValue = true;
3719 //protected Overrides void Finalize()
3721 // // このコードを変更しないでください。クリーンアップ コードを上の Dispose(bool disposing) に記述します。
3723 // MyBase.Finalize()
3726 // このコードは、破棄可能なパターンを正しく実装できるように Visual Basic によって追加されました。
3727 public void Dispose()
3729 // このコードを変更しないでください。クリーンアップ コードを上の Dispose(bool disposing) に記述します。
3731 GC.SuppressFinalize(this);
3736 public class PostDeletedEventArgs : EventArgs
3738 public long StatusId { get; }
3740 public PostDeletedEventArgs(long statusId)
3742 this.StatusId = statusId;
3746 public class UserStreamEventReceivedEventArgs : EventArgs
3748 public Twitter.FormattedEvent EventData { get; }
3750 public UserStreamEventReceivedEventArgs(Twitter.FormattedEvent eventData)
3752 this.EventData = eventData;