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 _restrictFavCheck;
159 private bool _readOwnPost;
160 private List<string> _hashList = new List<string>();
162 //max_idで古い発言を取得するために保持(lists分は個別タブで管理)
163 private long minHomeTimeline = long.MaxValue;
164 private long minMentions = long.MaxValue;
165 private long minDirectmessage = long.MaxValue;
166 private long minDirectmessageSent = long.MaxValue;
168 //private FavoriteQueue favQueue;
170 private HttpTwitter twCon = new HttpTwitter();
172 //private List<PostClass> _deletemessages = new List<PostClass>();
176 this.Configuration = TwitterConfiguration.DefaultConfiguration();
179 public TwitterApiAccessLevel AccessLevel
183 return MyCommon.TwitterApiInfo.AccessLevel;
187 protected void ResetApiStatus()
189 MyCommon.TwitterApiInfo.Reset();
192 public void Authenticate(string username, string password)
194 this.ResetApiStatus();
200 res = twCon.AuthUserAndPass(username, password, ref content);
204 throw new WebApiException("Err:" + ex.Message, ex);
207 this.CheckStatusCode(res, content);
209 _uname = username.ToLower();
210 if (SettingCommon.Instance.UserstreamStartup) this.ReconnectUserStream();
213 public string StartAuthentication()
216 this.ResetApiStatus();
219 string pinPageUrl = null;
220 var res = twCon.AuthGetRequestToken(ref pinPageUrl);
222 throw new WebApiException("Err:Failed to access auth server.");
228 throw new WebApiException("Err:Failed to access auth server.", ex);
232 public void Authenticate(string pinCode)
234 this.ResetApiStatus();
239 res = twCon.AuthGetAccessToken(pinCode);
243 throw new WebApiException("Err:Failed to access auth acc server.", ex);
246 this.CheckStatusCode(res, null);
248 _uname = Username.ToLower();
249 if (SettingCommon.Instance.UserstreamStartup) this.ReconnectUserStream();
252 public void ClearAuthInfo()
254 Twitter.AccountState = MyCommon.ACCOUNT_STATE.Invalid;
255 this.ResetApiStatus();
256 twCon.ClearAuthInfo();
259 public void VerifyCredentials()
265 res = twCon.VerifyCredentials(ref content);
269 throw new WebApiException("Err:" + ex.Message, ex);
272 this.CheckStatusCode(res, content);
276 var user = TwitterUser.ParseJson(content);
278 this.twCon.AuthenticatedUserId = user.Id;
279 this.UpdateUserStats(user);
281 catch (SerializationException ex)
283 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
284 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
288 public void Initialize(string token, string tokenSecret, string username, long userId)
291 if (string.IsNullOrEmpty(token) || string.IsNullOrEmpty(tokenSecret) || string.IsNullOrEmpty(username))
293 Twitter.AccountState = MyCommon.ACCOUNT_STATE.Invalid;
295 this.ResetApiStatus();
296 twCon.Initialize(token, tokenSecret, username, userId);
297 _uname = username.ToLower();
298 if (SettingCommon.Instance.UserstreamStartup) this.ReconnectUserStream();
301 public string PreProcessUrl(string orgData)
305 //var IDNConveter = new IdnMapping();
306 var href = "<a href=\"";
310 if (orgData.IndexOf(href, posl2, StringComparison.Ordinal) > -1)
314 posl1 = orgData.IndexOf(href, posl2, StringComparison.Ordinal);
315 posl1 += href.Length;
316 posl2 = orgData.IndexOf("\"", posl1, StringComparison.Ordinal);
317 urlStr = orgData.Substring(posl1, posl2 - posl1);
319 if (!urlStr.StartsWith("http://") && !urlStr.StartsWith("https://") && !urlStr.StartsWith("ftp://"))
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
1136 _restrictFavCheck = value;
1141 public void GetTweenBinary(string strVer)
1146 if (!(new HttpVarious()).GetDataToFile("http://tween.sourceforge.jp/Tween" + strVer + ".gz?" + DateTime.Now.ToString("yyMMddHHmmss") + Environment.TickCount.ToString(),
1147 Path.Combine(MyCommon.settingPath, "TweenNew.exe")))
1149 throw new WebApiException("Err:Download failed");
1152 if (!Directory.Exists(Path.Combine(MyCommon.settingPath, "en")))
1154 Directory.CreateDirectory(Path.Combine(MyCommon.settingPath, "en"));
1156 if (!(new HttpVarious()).GetDataToFile("http://tween.sourceforge.jp/TweenResEn" + strVer + ".gz?" + DateTime.Now.ToString("yyMMddHHmmss") + Environment.TickCount.ToString(),
1157 Path.Combine(Path.Combine(MyCommon.settingPath, "en"), "Tween.resourcesNew.dll")))
1159 throw new WebApiException("Err:Download failed");
1161 //その他言語圏のリソース。取得失敗しても継続
1164 if (!Thread.CurrentThread.CurrentUICulture.IsNeutralCulture)
1166 var idx = Thread.CurrentThread.CurrentUICulture.Name.LastIndexOf('-');
1169 curCul = Thread.CurrentThread.CurrentUICulture.Name.Substring(0, idx);
1173 curCul = Thread.CurrentThread.CurrentUICulture.Name;
1178 curCul = Thread.CurrentThread.CurrentUICulture.Name;
1180 if (!string.IsNullOrEmpty(curCul) && curCul != "en" && curCul != "ja")
1182 if (!Directory.Exists(Path.Combine(MyCommon.settingPath, curCul)))
1184 Directory.CreateDirectory(Path.Combine(MyCommon.settingPath, curCul));
1186 if (!(new HttpVarious()).GetDataToFile("http://tween.sourceforge.jp/TweenRes" + curCul + strVer + ".gz?" + DateTime.Now.ToString("yyMMddHHmmss") + Environment.TickCount.ToString(),
1187 Path.Combine(Path.Combine(MyCommon.settingPath, curCul), "Tween.resourcesNew.dll")))
1189 //return "Err:Download failed";
1194 if (!Thread.CurrentThread.CurrentCulture.IsNeutralCulture)
1196 var idx = Thread.CurrentThread.CurrentCulture.Name.LastIndexOf('-');
1199 curCul2 = Thread.CurrentThread.CurrentCulture.Name.Substring(0, idx);
1203 curCul2 = Thread.CurrentThread.CurrentCulture.Name;
1208 curCul2 = Thread.CurrentThread.CurrentCulture.Name;
1210 if (!string.IsNullOrEmpty(curCul2) && curCul2 != "en" && curCul2 != curCul)
1212 if (!Directory.Exists(Path.Combine(MyCommon.settingPath, curCul2)))
1214 Directory.CreateDirectory(Path.Combine(MyCommon.settingPath, curCul2));
1216 if (!(new HttpVarious()).GetDataToFile("http://tween.sourceforge.jp/TweenRes" + curCul2 + strVer + ".gz?" + DateTime.Now.ToString("yyMMddHHmmss") + Environment.TickCount.ToString(),
1217 Path.Combine(Path.Combine(MyCommon.settingPath, curCul2), "Tween.resourcesNew.dll")))
1219 //return "Err:Download failed";
1224 if (!(new HttpVarious()).GetDataToFile("http://tween.sourceforge.jp/TweenUp3.gz?" + DateTime.Now.ToString("yyMMddHHmmss") + Environment.TickCount.ToString(),
1225 Path.Combine(MyCommon.settingPath, "TweenUp3.exe")))
1227 throw new WebApiException("Err:Download failed");
1230 if (!(new HttpVarious()).GetDataToFile("http://tween.sourceforge.jp/TweenDll" + strVer + ".gz?" + DateTime.Now.ToString("yyMMddHHmmss") + Environment.TickCount.ToString(),
1231 Path.Combine(MyCommon.settingPath, "TweenNew.XmlSerializers.dll")))
1233 throw new WebApiException("Err:Download failed");
1236 catch (Exception ex)
1238 throw new WebApiException("Err:Download failed", ex);
1243 public bool ReadOwnPost
1247 return _readOwnPost;
1251 _readOwnPost = value;
1255 public int FollowersCount { get; private set; }
1256 public int FriendsCount { get; private set; }
1257 public int StatusesCount { get; private set; }
1258 public string Location { get; private set; } = "";
1259 public string Bio { get; private set; } = "";
1261 /// <summary>ユーザーのフォロワー数などの情報を更新します</summary>
1262 private void UpdateUserStats(TwitterUser self)
1264 this.FollowersCount = self.FollowersCount;
1265 this.FriendsCount = self.FriendsCount;
1266 this.StatusesCount = self.StatusesCount;
1267 this.Location = self.Location;
1268 this.Bio = self.Description;
1272 /// 渡された取得件数がWORKERTYPEに応じた取得可能範囲に収まっているか検証する
1274 public static bool VerifyApiResultCount(MyCommon.WORKERTYPE type, int count)
1276 return count >= 20 && count <= GetMaxApiResultCount(type);
1280 /// 渡された取得件数が更新時の取得可能範囲に収まっているか検証する
1282 public static bool VerifyMoreApiResultCount(int count)
1284 return count >= 20 && count <= 200;
1288 /// 渡された取得件数が起動時の取得可能範囲に収まっているか検証する
1290 public static bool VerifyFirstApiResultCount(int count)
1292 return count >= 20 && count <= 200;
1296 /// WORKERTYPEに応じた取得可能な最大件数を取得する
1298 public static int GetMaxApiResultCount(MyCommon.WORKERTYPE type)
1300 // 参照: REST APIs - 各endpointのcountパラメータ
1301 // https://dev.twitter.com/rest/public
1304 case MyCommon.WORKERTYPE.Timeline:
1305 case MyCommon.WORKERTYPE.Reply:
1306 case MyCommon.WORKERTYPE.UserTimeline:
1307 case MyCommon.WORKERTYPE.Favorites:
1308 case MyCommon.WORKERTYPE.DirectMessegeRcv:
1309 case MyCommon.WORKERTYPE.DirectMessegeSnt:
1310 case MyCommon.WORKERTYPE.List: // 不明
1313 case MyCommon.WORKERTYPE.PublicSearch:
1317 throw new InvalidOperationException("Invalid type: " + type);
1322 /// WORKERTYPEに応じた取得件数を取得する
1324 public static int GetApiResultCount(MyCommon.WORKERTYPE type, bool more, bool startup)
1326 if (type == MyCommon.WORKERTYPE.DirectMessegeRcv ||
1327 type == MyCommon.WORKERTYPE.DirectMessegeSnt)
1332 if (SettingCommon.Instance.UseAdditionalCount)
1336 case MyCommon.WORKERTYPE.Favorites:
1337 if (SettingCommon.Instance.FavoritesCountApi != 0)
1338 return SettingCommon.Instance.FavoritesCountApi;
1340 case MyCommon.WORKERTYPE.List:
1341 if (SettingCommon.Instance.ListCountApi != 0)
1342 return SettingCommon.Instance.ListCountApi;
1344 case MyCommon.WORKERTYPE.PublicSearch:
1345 if (SettingCommon.Instance.SearchCountApi != 0)
1346 return SettingCommon.Instance.SearchCountApi;
1348 case MyCommon.WORKERTYPE.UserTimeline:
1349 if (SettingCommon.Instance.UserTimelineCountApi != 0)
1350 return SettingCommon.Instance.UserTimelineCountApi;
1353 if (more && SettingCommon.Instance.MoreCountApi != 0)
1355 return Math.Min(SettingCommon.Instance.MoreCountApi, GetMaxApiResultCount(type));
1357 if (startup && SettingCommon.Instance.FirstCountApi != 0 && type != MyCommon.WORKERTYPE.Reply)
1359 return Math.Min(SettingCommon.Instance.FirstCountApi, GetMaxApiResultCount(type));
1363 // 上記に当てはまらない場合の共通処理
1364 var count = SettingCommon.Instance.CountApi;
1366 if (type == MyCommon.WORKERTYPE.Reply)
1367 count = SettingCommon.Instance.CountApiReply;
1369 return Math.Min(count, GetMaxApiResultCount(type));
1372 public void GetTimelineApi(bool read,
1373 MyCommon.WORKERTYPE gType,
1377 this.CheckAccountState();
1381 var count = GetApiResultCount(gType, more, startup);
1385 if (gType == MyCommon.WORKERTYPE.Timeline)
1389 res = twCon.HomeTimeline(count, this.minHomeTimeline, null, ref content);
1393 res = twCon.HomeTimeline(count, null, null, ref content);
1400 res = twCon.Mentions(count, this.minMentions, null, ref content);
1404 res = twCon.Mentions(count, null, null, ref content);
1410 throw new WebApiException("Err:" + ex.Message, ex);
1413 this.CheckStatusCode(res, content);
1415 var minimumId = CreatePostsFromJson(content, gType, null, read);
1417 if (minimumId != null)
1419 if (gType == MyCommon.WORKERTYPE.Timeline)
1420 this.minHomeTimeline = minimumId.Value;
1422 this.minMentions = minimumId.Value;
1426 public void GetUserTimelineApi(bool read,
1431 this.CheckAccountState();
1435 var count = GetApiResultCount(MyCommon.WORKERTYPE.UserTimeline, more, false);
1439 if (string.IsNullOrEmpty(userName))
1441 var target = tab.User;
1442 if (string.IsNullOrEmpty(target)) return;
1444 res = twCon.UserTimeline(null, target, count, null, null, ref content);
1450 res = twCon.UserTimeline(null, userName, count, tab.OldestId, null, ref content);
1454 res = twCon.UserTimeline(null, userName, count, null, null, ref content);
1460 throw new WebApiException("Err:" + ex.Message, ex);
1463 if (res == HttpStatusCode.Unauthorized)
1464 throw new WebApiException("Err:@" + userName + "'s Tweets are protected.");
1466 this.CheckStatusCode(res, content);
1468 var minimumId = CreatePostsFromJson(content, MyCommon.WORKERTYPE.UserTimeline, tab, read);
1470 if (minimumId != null)
1471 tab.OldestId = minimumId.Value;
1474 public PostClass GetStatusApi(bool read, long id)
1476 this.CheckAccountState();
1482 res = twCon.ShowStatuses(id, ref content);
1486 throw new WebApiException("Err:" + ex.Message, ex);
1489 if (res == HttpStatusCode.Forbidden)
1490 throw new WebApiException("Err:protected user's tweet", content);
1492 this.CheckStatusCode(res, content);
1494 TwitterStatus status;
1497 status = TwitterStatus.ParseJson(content);
1499 catch(SerializationException ex)
1501 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
1502 throw new WebApiException("Json Parse Error(DataContractJsonSerializer)", content, ex);
1506 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
1507 throw new WebApiException("Invalid Json!", content, ex);
1510 var item = CreatePostsFromStatusData(status);
1512 throw new WebApiException("Err:Can't create post", content);
1515 if (item.IsMe && !read && _readOwnPost) item.IsRead = true;
1520 public void GetStatusApi(bool read, long id, TabClass tab)
1522 var post = this.GetStatusApi(read, id);
1524 //非同期アイコン取得&StatusDictionaryに追加
1525 if (tab != null && tab.IsInnerStorageTabType)
1526 tab.AddPostToInnerStorage(post);
1528 TabInformations.GetInstance().AddPost(post);
1531 private PostClass CreatePostsFromStatusData(TwitterStatus status)
1533 return CreatePostsFromStatusData(status, false);
1536 private PostClass CreatePostsFromStatusData(TwitterStatus status, bool favTweet)
1538 var post = new PostClass();
1539 TwitterEntities entities;
1542 post.StatusId = status.Id;
1543 if (status.RetweetedStatus != null)
1545 var retweeted = status.RetweetedStatus;
1547 post.CreatedAt = MyCommon.DateTimeParse(retweeted.CreatedAt);
1550 post.RetweetedId = retweeted.Id;
1552 post.TextFromApi = retweeted.Text;
1553 entities = retweeted.MergedEntities;
1554 sourceHtml = retweeted.Source;
1556 post.InReplyToStatusId = retweeted.InReplyToStatusId;
1557 post.InReplyToUser = retweeted.InReplyToScreenName;
1558 post.InReplyToUserId = status.InReplyToUserId;
1567 var tc = TabInformations.GetInstance().GetTabByType(MyCommon.TabUsageType.Favorites);
1568 post.IsFav = tc.Contains(retweeted.Id);
1571 if (retweeted.Coordinates != null)
1572 post.PostGeo = new PostClass.StatusGeo(retweeted.Coordinates.Coordinates[0], retweeted.Coordinates.Coordinates[1]);
1575 var user = retweeted.User;
1577 if (user == null || user.ScreenName == null || status.User.ScreenName == null) return null;
1579 post.UserId = user.Id;
1580 post.ScreenName = user.ScreenName;
1581 post.Nickname = user.Name.Trim();
1582 post.ImageUrl = user.ProfileImageUrlHttps;
1583 post.IsProtect = user.Protected;
1586 post.RetweetedBy = status.User.ScreenName;
1587 post.RetweetedByUserId = status.User.Id;
1588 post.IsMe = post.RetweetedBy.ToLower().Equals(_uname);
1592 post.CreatedAt = MyCommon.DateTimeParse(status.CreatedAt);
1594 post.TextFromApi = status.Text;
1595 entities = status.MergedEntities;
1596 sourceHtml = status.Source;
1597 post.InReplyToStatusId = status.InReplyToStatusId;
1598 post.InReplyToUser = status.InReplyToScreenName;
1599 post.InReplyToUserId = status.InReplyToUserId;
1608 var tc = TabInformations.GetInstance().GetTabByType(MyCommon.TabUsageType.Favorites);
1609 post.IsFav = tc.Contains(post.StatusId) && TabInformations.GetInstance()[post.StatusId].IsFav;
1612 if (status.Coordinates != null)
1613 post.PostGeo = new PostClass.StatusGeo(status.Coordinates.Coordinates[0], status.Coordinates.Coordinates[1]);
1616 var user = status.User;
1618 if (user == null || user.ScreenName == null) return null;
1620 post.UserId = user.Id;
1621 post.ScreenName = user.ScreenName;
1622 post.Nickname = user.Name.Trim();
1623 post.ImageUrl = user.ProfileImageUrlHttps;
1624 post.IsProtect = user.Protected;
1625 post.IsMe = post.ScreenName.ToLower().Equals(_uname);
1628 string textFromApi = post.TextFromApi;
1629 post.Text = CreateHtmlAnchor(textFromApi, post.ReplyToList, entities, post.Media);
1630 post.TextFromApi = textFromApi;
1631 post.TextFromApi = this.ReplaceTextFromApi(post.TextFromApi, entities);
1632 post.TextFromApi = WebUtility.HtmlDecode(post.TextFromApi);
1633 post.TextFromApi = post.TextFromApi.Replace("<3", "\u2661");
1635 post.QuoteStatusIds = GetQuoteTweetStatusIds(entities)
1636 .Where(x => x != post.StatusId && x != post.RetweetedId)
1637 .Distinct().ToArray();
1639 post.ExpandedUrls = entities.OfType<TwitterEntityUrl>()
1640 .Where(x => x != null)
1641 .GroupBy(x => x.Url)
1642 .ToDictionary(x => x.Key, x => new PostClass.ExpandedUrlInfo(x.Key, x.First().ExpandedUrl));
1645 var source = ParseSource(sourceHtml);
1646 post.Source = source.Item1;
1647 post.SourceUri = source.Item2;
1649 post.IsReply = post.ReplyToList.Contains(_uname);
1650 post.IsExcludeReply = false;
1658 if (followerId.Count > 0) post.IsOwl = !followerId.Contains(post.UserId);
1666 /// ツイートに含まれる引用ツイートのURLからステータスIDを抽出
1668 public static IEnumerable<long> GetQuoteTweetStatusIds(IEnumerable<TwitterEntity> entities)
1670 var urls = entities.OfType<TwitterEntityUrl>().Where(x => x != null)
1671 .Select(x => x.ExpandedUrl);
1673 return GetQuoteTweetStatusIds(urls);
1676 public static IEnumerable<long> GetQuoteTweetStatusIds(IEnumerable<string> urls)
1678 foreach (var url in urls)
1680 var match = Twitter.StatusUrlRegex.Match(url);
1684 if (long.TryParse(match.Groups["StatusId"].Value, out statusId))
1685 yield return statusId;
1690 private long? CreatePostsFromJson(string content, MyCommon.WORKERTYPE gType, TabClass tab, bool read)
1692 TwitterStatus[] items;
1695 items = TwitterStatus.ParseJsonArray(content);
1697 catch(SerializationException ex)
1699 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
1700 throw new WebApiException("Json Parse Error(DataContractJsonSerializer)", content, ex);
1704 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
1705 throw new WebApiException("Invalid Json!", content, ex);
1708 long? minimumId = null;
1710 foreach (var status in items)
1712 PostClass post = null;
1713 post = CreatePostsFromStatusData(status);
1714 if (post == null) continue;
1716 if (minimumId == null || minimumId.Value > post.StatusId)
1717 minimumId = post.StatusId;
1724 if (TabInformations.GetInstance().ContainsKey(post.StatusId)) continue;
1728 if (tab.Contains(post.StatusId)) continue;
1733 if (gType != MyCommon.WORKERTYPE.UserTimeline &&
1734 post.RetweetedByUserId != null && this.noRTId.Contains(post.RetweetedByUserId.Value)) continue;
1737 if (post.IsMe && !read && _readOwnPost) post.IsRead = true;
1739 //非同期アイコン取得&StatusDictionaryに追加
1740 if (tab != null && tab.IsInnerStorageTabType)
1741 tab.AddPostToInnerStorage(post);
1743 TabInformations.GetInstance().AddPost(post);
1749 private long? CreatePostsFromSearchJson(string content, TabClass tab, bool read, int count, bool more)
1751 TwitterSearchResult items;
1754 items = TwitterSearchResult.ParseJson(content);
1756 catch (SerializationException ex)
1758 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
1759 throw new WebApiException("Json Parse Error(DataContractJsonSerializer)", content, ex);
1761 catch (Exception ex)
1763 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
1764 throw new WebApiException("Invalid Json!", content, ex);
1767 long? minimumId = null;
1769 foreach (var result in items.Statuses)
1771 PostClass post = null;
1772 post = CreatePostsFromStatusData(result);
1776 // Search API は相変わらずぶっ壊れたデータを返すことがあるため、必要なデータが欠如しているものは取得し直す
1779 post = this.GetStatusApi(read, result.Id);
1781 catch (WebApiException)
1787 if (minimumId == null || minimumId.Value > post.StatusId)
1788 minimumId = post.StatusId;
1790 if (!more && post.StatusId > tab.SinceId) tab.SinceId = post.StatusId;
1796 if (TabInformations.GetInstance().ContainsKey(post.StatusId)) continue;
1800 if (tab.Contains(post.StatusId)) continue;
1805 if ((post.IsMe && !read) && this._readOwnPost) post.IsRead = true;
1807 //非同期アイコン取得&StatusDictionaryに追加
1808 if (tab != null && tab.IsInnerStorageTabType)
1809 tab.AddPostToInnerStorage(post);
1811 TabInformations.GetInstance().AddPost(post);
1817 private void CreateFavoritePostsFromJson(string content, bool read)
1819 TwitterStatus[] item;
1822 item = TwitterStatus.ParseJsonArray(content);
1824 catch (SerializationException ex)
1826 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
1827 throw new WebApiException("Json Parse Error(DataContractJsonSerializer)", content, ex);
1829 catch (Exception ex)
1831 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
1832 throw new WebApiException("Invalid Json!", content, ex);
1835 var favTab = TabInformations.GetInstance().GetTabByType(MyCommon.TabUsageType.Favorites);
1837 foreach (var status in item)
1842 if (favTab.Contains(status.Id)) continue;
1845 var post = CreatePostsFromStatusData(status, true);
1846 if (post == null) continue;
1850 TabInformations.GetInstance().AddPost(post);
1854 public void GetListStatus(bool read,
1861 var count = GetApiResultCount(MyCommon.WORKERTYPE.List, more, startup);
1867 res = twCon.GetListsStatuses(tab.ListInfo.UserId, tab.ListInfo.Id, count, tab.OldestId, null, SettingCommon.Instance.IsListsIncludeRts, ref content);
1871 res = twCon.GetListsStatuses(tab.ListInfo.UserId, tab.ListInfo.Id, count, null, null, SettingCommon.Instance.IsListsIncludeRts, ref content);
1876 throw new WebApiException("Err:" + ex.Message, ex);
1879 this.CheckStatusCode(res, content);
1881 var minimumId = CreatePostsFromJson(content, MyCommon.WORKERTYPE.List, tab, read);
1883 if (minimumId != null)
1884 tab.OldestId = minimumId.Value;
1888 /// startStatusId からリプライ先の発言を辿る。発言は posts 以外からは検索しない。
1890 /// <returns>posts の中から検索されたリプライチェインの末端</returns>
1891 internal static PostClass FindTopOfReplyChain(IDictionary<Int64, PostClass> posts, Int64 startStatusId)
1893 if (!posts.ContainsKey(startStatusId))
1894 throw new ArgumentException("startStatusId (" + startStatusId + ") が posts の中から見つかりませんでした。", nameof(startStatusId));
1896 var nextPost = posts[startStatusId];
1897 while (nextPost.InReplyToStatusId != null)
1899 if (!posts.ContainsKey(nextPost.InReplyToStatusId.Value))
1901 nextPost = posts[nextPost.InReplyToStatusId.Value];
1907 public void GetRelatedResult(bool read, TabClass tab)
1909 var relPosts = new Dictionary<Int64, PostClass>();
1910 if (tab.RelationTargetPost.TextFromApi.Contains("@") && tab.RelationTargetPost.InReplyToStatusId == null)
1913 var p = TabInformations.GetInstance()[tab.RelationTargetPost.StatusId];
1914 if (p != null && p.InReplyToStatusId != null)
1916 tab.RelationTargetPost = p;
1920 p = this.GetStatusApi(read, tab.RelationTargetPost.StatusId);
1921 tab.RelationTargetPost = p;
1924 relPosts.Add(tab.RelationTargetPost.StatusId, tab.RelationTargetPost);
1926 Exception lastException = null;
1928 // in_reply_to_status_id を使用してリプライチェインを辿る
1929 var nextPost = FindTopOfReplyChain(relPosts, tab.RelationTargetPost.StatusId);
1931 while (nextPost.InReplyToStatusId != null && loopCount++ <= 20)
1933 var inReplyToId = nextPost.InReplyToStatusId.Value;
1935 var inReplyToPost = TabInformations.GetInstance()[inReplyToId];
1936 if (inReplyToPost == null)
1940 inReplyToPost = this.GetStatusApi(read, inReplyToId);
1942 catch (WebApiException ex)
1949 relPosts.Add(inReplyToPost.StatusId, inReplyToPost);
1951 nextPost = FindTopOfReplyChain(relPosts, nextPost.StatusId);
1954 //MRTとかに対応のためツイート内にあるツイートを指すURLを取り込む
1955 var text = tab.RelationTargetPost.Text;
1956 var ma = Twitter.StatusUrlRegex.Matches(text).Cast<Match>()
1957 .Concat(Twitter.ThirdPartyStatusUrlRegex.Matches(text).Cast<Match>());
1958 foreach (var _match in ma)
1961 if (Int64.TryParse(_match.Groups["StatusId"].Value, out _statusId))
1963 if (relPosts.ContainsKey(_statusId))
1966 var p = TabInformations.GetInstance()[_statusId];
1971 p = this.GetStatusApi(read, _statusId);
1973 catch (WebApiException ex)
1981 relPosts.Add(p.StatusId, p);
1985 relPosts.Values.ToList().ForEach(p =>
1987 if (p.IsMe && !read && this._readOwnPost)
1992 tab.AddPostToInnerStorage(p);
1995 if (lastException != null)
1996 throw new WebApiException(lastException.Message, lastException);
1999 public void GetSearch(bool read,
2005 var count = GetApiResultCount(MyCommon.WORKERTYPE.PublicSearch, more, false);
2007 long? sinceId = null;
2010 maxId = tab.OldestId - 1;
2014 sinceId = tab.SinceId;
2019 // TODO:一時的に40>100件に 件数変更UI作成の必要あり
2020 res = twCon.Search(tab.SearchWords, tab.SearchLang, count, maxId, sinceId, ref content);
2024 throw new WebApiException("Err:" + ex.Message, ex);
2028 case HttpStatusCode.BadRequest:
2029 throw new WebApiException("Invalid query", content);
2030 case HttpStatusCode.NotFound:
2031 throw new WebApiException("Invalid query", content);
2032 case HttpStatusCode.PaymentRequired: //API Documentには420と書いてあるが、該当コードがないので402にしてある
2033 throw new WebApiException("Search API Limit?", content);
2034 case HttpStatusCode.OK:
2037 throw new WebApiException("Err:" + res.ToString() + "(" + MethodBase.GetCurrentMethod().Name + ")", content);
2040 if (!TabInformations.GetInstance().ContainsTab(tab))
2043 var minimumId = this.CreatePostsFromSearchJson(content, tab, read, count, more);
2045 if (minimumId != null)
2046 tab.OldestId = minimumId.Value;
2049 private void CreateDirectMessagesFromJson(string content, MyCommon.WORKERTYPE gType, bool read)
2051 TwitterDirectMessage[] item;
2054 if (gType == MyCommon.WORKERTYPE.UserStream)
2056 item = new[] { TwitterStreamEventDirectMessage.ParseJson(content).DirectMessage };
2060 item = TwitterDirectMessage.ParseJsonArray(content);
2063 catch(SerializationException ex)
2065 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
2066 throw new WebApiException("Json Parse Error(DataContractJsonSerializer)", content, ex);
2070 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
2071 throw new WebApiException("Invalid Json!", content, ex);
2074 foreach (var message in item)
2076 var post = new PostClass();
2079 post.StatusId = message.Id;
2080 if (gType != MyCommon.WORKERTYPE.UserStream)
2082 if (gType == MyCommon.WORKERTYPE.DirectMessegeRcv)
2084 if (minDirectmessage > post.StatusId) minDirectmessage = post.StatusId;
2088 if (minDirectmessageSent > post.StatusId) minDirectmessageSent = post.StatusId;
2095 if (TabInformations.GetInstance().GetTabByType(MyCommon.TabUsageType.DirectMessage).Contains(post.StatusId)) continue;
2099 post.CreatedAt = MyCommon.DateTimeParse(message.CreatedAt);
2101 var textFromApi = message.Text;
2103 post.Text = CreateHtmlAnchor(textFromApi, post.ReplyToList, message.Entities, post.Media);
2104 post.TextFromApi = this.ReplaceTextFromApi(textFromApi, message.Entities);
2105 post.TextFromApi = WebUtility.HtmlDecode(post.TextFromApi);
2106 post.TextFromApi = post.TextFromApi.Replace("<3", "\u2661");
2109 post.QuoteStatusIds = GetQuoteTweetStatusIds(message.Entities).Distinct().ToArray();
2111 post.ExpandedUrls = message.Entities.OfType<TwitterEntityUrl>()
2112 .Where(x => x != null)
2113 .GroupBy(x => x.Url)
2114 .ToDictionary(x => x.Key, x => new PostClass.ExpandedUrlInfo(x.Key, x.First().ExpandedUrl));
2118 if (gType == MyCommon.WORKERTYPE.UserStream)
2120 if (twCon.AuthenticatedUsername.Equals(message.Recipient.ScreenName, StringComparison.CurrentCultureIgnoreCase))
2122 user = message.Sender;
2128 user = message.Recipient;
2135 if (gType == MyCommon.WORKERTYPE.DirectMessegeRcv)
2137 user = message.Sender;
2143 user = message.Recipient;
2149 post.UserId = user.Id;
2150 post.ScreenName = user.ScreenName;
2151 post.Nickname = user.Name.Trim();
2152 post.ImageUrl = user.ProfileImageUrlHttps;
2153 post.IsProtect = user.Protected;
2157 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
2158 MessageBox.Show("Parse Error(CreateDirectMessagesFromJson)");
2163 if (post.IsMe && !read && _readOwnPost) post.IsRead = true;
2164 post.IsReply = false;
2165 post.IsExcludeReply = false;
2168 var dmTab = TabInformations.GetInstance().GetTabByType(MyCommon.TabUsageType.DirectMessage);
2169 dmTab.AddPostToInnerStorage(post);
2173 public void GetDirectMessageApi(bool read,
2174 MyCommon.WORKERTYPE gType,
2177 this.CheckAccountState();
2178 this.CheckAccessLevel(TwitterApiAccessLevel.ReadWriteAndDirectMessage);
2182 var count = GetApiResultCount(gType, more, false);
2186 if (gType == MyCommon.WORKERTYPE.DirectMessegeRcv)
2190 res = twCon.DirectMessages(count, minDirectmessage, null, ref content);
2194 res = twCon.DirectMessages(count, null, null, ref content);
2201 res = twCon.DirectMessagesSent(count, minDirectmessageSent, null, ref content);
2205 res = twCon.DirectMessagesSent(count, null, null, ref content);
2211 throw new WebApiException("Err:" + ex.Message, ex);
2214 this.CheckStatusCode(res, content);
2216 CreateDirectMessagesFromJson(content, gType, read);
2219 public void GetFavoritesApi(bool read,
2222 this.CheckAccountState();
2226 var count = GetApiResultCount(MyCommon.WORKERTYPE.Favorites, more, false);
2230 res = twCon.Favorites(count, ref content);
2234 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
2237 this.CheckStatusCode(res, content);
2239 CreateFavoritePostsFromJson(content, read);
2242 private string ReplaceTextFromApi(string text, TwitterEntities entities)
2244 if (entities != null)
2246 if (entities.Urls != null)
2248 foreach (var m in entities.Urls)
2250 if (!string.IsNullOrEmpty(m.DisplayUrl)) text = text.Replace(m.Url, m.DisplayUrl);
2253 if (entities.Media != null)
2255 foreach (var m in entities.Media)
2257 if (!string.IsNullOrEmpty(m.DisplayUrl)) text = text.Replace(m.Url, m.DisplayUrl);
2267 /// <exception cref="WebApiException"/>
2268 public void RefreshFollowerIds()
2270 if (MyCommon._endingFlag) return;
2273 var newFollowerIds = new HashSet<long>();
2276 var ret = this.GetFollowerIdsApi(ref cursor);
2277 newFollowerIds.UnionWith(ret.Ids);
2278 cursor = ret.NextCursor;
2279 } while (cursor != 0);
2281 this.followerId = newFollowerIds;
2282 TabInformations.GetInstance().RefreshOwl(this.followerId);
2284 this._GetFollowerResult = true;
2287 public bool GetFollowersSuccess
2291 return _GetFollowerResult;
2295 private TwitterIds GetFollowerIdsApi(ref long cursor)
2297 this.CheckAccountState();
2303 res = twCon.FollowerIds(cursor, ref content);
2307 throw new WebApiException("Err:" + e.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", e);
2310 this.CheckStatusCode(res, content);
2314 var ret = TwitterIds.ParseJson(content);
2316 if (ret.Ids == null)
2318 var ex = new WebApiException("Err: ret.id == null (GetFollowerIdsApi)", content);
2319 MyCommon.ExceptionOut(ex);
2325 catch(SerializationException e)
2327 var ex = new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, e);
2328 MyCommon.TraceOut(ex);
2333 var ex = new WebApiException("Err:Invalid Json!", content, e);
2334 MyCommon.TraceOut(ex);
2340 /// RT 非表示ユーザーを更新します
2342 /// <exception cref="WebApiException"/>
2343 public void RefreshNoRetweetIds()
2345 if (MyCommon._endingFlag) return;
2347 this.noRTId = this.NoRetweetIdsApi();
2349 this._GetNoRetweetResult = true;
2352 private long[] NoRetweetIdsApi()
2354 this.CheckAccountState();
2360 res = twCon.NoRetweetIds(ref content);
2364 throw new WebApiException("Err:" + e.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", e);
2367 this.CheckStatusCode(res, content);
2371 return MyCommon.CreateDataFromJson<long[]>(content);
2373 catch(SerializationException e)
2375 var ex = new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, e);
2376 MyCommon.TraceOut(ex);
2381 var ex = new WebApiException("Err:Invalid Json!", content, e);
2382 MyCommon.TraceOut(ex);
2387 public bool GetNoRetweetSuccess
2391 return _GetNoRetweetResult;
2396 /// t.co の文字列長などの設定情報を更新します
2398 /// <exception cref="WebApiException"/>
2399 public void RefreshConfiguration()
2401 this.Configuration = this.ConfigurationApi();
2404 private TwitterConfiguration ConfigurationApi()
2410 res = twCon.GetConfiguration(ref content);
2414 throw new WebApiException("Err:" + e.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", e);
2417 this.CheckStatusCode(res, content);
2421 return TwitterConfiguration.ParseJson(content);
2423 catch(SerializationException e)
2425 var ex = new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, e);
2426 MyCommon.TraceOut(ex);
2431 var ex = new WebApiException("Err:Invalid Json!", content, e);
2432 MyCommon.TraceOut(ex);
2437 public void GetListsApi()
2439 this.CheckAccountState();
2442 IEnumerable<ListElement> lists;
2447 res = twCon.GetLists(this.Username, ref content);
2449 catch (Exception ex)
2451 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
2454 this.CheckStatusCode(res, content);
2458 lists = TwitterList.ParseJsonArray(content)
2459 .Select(x => new ListElement(x, this));
2461 catch (SerializationException ex)
2463 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
2464 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
2466 catch (Exception ex)
2468 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
2469 throw new WebApiException("Err:Invalid Json!", content, ex);
2474 res = twCon.GetListsSubscriptions(this.Username, ref content);
2476 catch (Exception ex)
2478 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
2481 this.CheckStatusCode(res, content);
2485 lists = lists.Concat(TwitterList.ParseJsonArray(content)
2486 .Select(x => new ListElement(x, this)));
2488 catch (SerializationException ex)
2490 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
2491 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
2493 catch (Exception ex)
2495 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
2496 throw new WebApiException("Err:Invalid Json!", content, ex);
2499 TabInformations.GetInstance().SubscribableLists = lists.ToList();
2502 public void DeleteList(string list_id)
2509 res = twCon.DeleteListID(this.Username, list_id, ref content);
2513 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
2516 this.CheckStatusCode(res, content);
2519 public ListElement EditList(string list_id, string new_name, bool isPrivate, string description)
2526 res = twCon.UpdateListID(this.Username, list_id, new_name, isPrivate, description, ref content);
2530 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
2533 this.CheckStatusCode(res, content);
2537 var le = TwitterList.ParseJson(content);
2538 return new ListElement(le, this);
2540 catch(SerializationException ex)
2542 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
2543 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
2547 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
2548 throw new WebApiException("Err:Invalid Json!", content, ex);
2552 public long GetListMembers(string list_id, List<UserInfo> lists, long cursor)
2554 this.CheckAccountState();
2560 res = twCon.GetListMembers(this.Username, list_id, cursor, ref content);
2564 throw new WebApiException("Err:" + ex.Message);
2567 this.CheckStatusCode(res, content);
2571 var users = TwitterUsers.ParseJson(content);
2572 Array.ForEach<TwitterUser>(
2574 u => lists.Add(new UserInfo(u)));
2576 return users.NextCursor;
2578 catch(SerializationException ex)
2580 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
2581 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
2585 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
2586 throw new WebApiException("Err:Invalid Json!", content, ex);
2590 public void CreateListApi(string listName, bool isPrivate, string description)
2592 this.CheckAccountState();
2598 res = twCon.CreateLists(listName, isPrivate, description, ref content);
2602 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
2605 this.CheckStatusCode(res, content);
2609 var le = TwitterList.ParseJson(content);
2610 TabInformations.GetInstance().SubscribableLists.Add(new ListElement(le, this));
2612 catch(SerializationException ex)
2614 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
2615 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
2619 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
2620 throw new WebApiException("Err:Invalid Json!", content, ex);
2624 public bool ContainsUserAtList(string listId, string user)
2626 this.CheckAccountState();
2633 res = this.twCon.ShowListMember(listId, user, ref content);
2637 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
2640 if (res == HttpStatusCode.NotFound)
2645 this.CheckStatusCode(res, content);
2649 TwitterUser.ParseJson(content);
2658 public void AddUserToList(string listId, string user)
2665 res = twCon.CreateListMembers(listId, user, ref content);
2669 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
2672 this.CheckStatusCode(res, content);
2675 public void RemoveUserToList(string listId, string user)
2682 res = twCon.DeleteListMembers(listId, user, ref content);
2686 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
2689 this.CheckStatusCode(res, content);
2692 public string CreateHtmlAnchor(string text, List<string> AtList, TwitterEntities entities, List<MediaInfo> media)
2694 if (entities != null)
2696 if (entities.Hashtags != null)
2700 this._hashList.AddRange(entities.Hashtags.Select(x => "#" + x.Text));
2703 if (entities.UserMentions != null)
2705 foreach (var ent in entities.UserMentions)
2707 var screenName = ent.ScreenName.ToLower();
2708 if (!AtList.Contains(screenName))
2709 AtList.Add(screenName);
2712 if (entities.Media != null)
2716 foreach (var ent in entities.Media)
2718 if (!media.Any(x => x.Url == ent.MediaUrl))
2720 if (ent.VideoInfo != null &&
2721 ent.Type == "animated_gif" || ent.Type == "video")
2723 //var videoUrl = ent.VideoInfo.Variants
2724 // .Where(v => v.ContentType == "video/mp4")
2725 // .OrderByDescending(v => v.Bitrate)
2726 // .Select(v => v.Url).FirstOrDefault();
2727 media.Add(new MediaInfo(ent.MediaUrl, ent.ExpandedUrl));
2730 media.Add(new MediaInfo(ent.MediaUrl));
2737 // PostClass.ExpandedUrlInfo を使用して非同期に URL 展開を行うためここでは expanded_url を使用しない
2738 text = TweetFormatter.AutoLinkHtml(text, entities, keepTco: true);
2740 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>");
2741 text = PreProcessUrl(text); //IDN置換
2747 /// Twitter APIから得たHTML形式のsource文字列を分析し、source名とURLに分離します
2749 public static Tuple<string, Uri> ParseSource(string sourceHtml)
2751 if (string.IsNullOrEmpty(sourceHtml))
2752 return Tuple.Create<string, Uri>("", null);
2757 // sourceHtmlの例: <a href="http://twitter.com" rel="nofollow">Twitter Web Client</a>
2759 var match = Regex.Match(sourceHtml, "^<a href=\"(?<uri>.+?)\".*?>(?<text>.+)</a>$", RegexOptions.IgnoreCase);
2762 sourceText = WebUtility.HtmlDecode(match.Groups["text"].Value);
2765 var uriStr = WebUtility.HtmlDecode(match.Groups["uri"].Value);
2766 sourceUri = new Uri(new Uri("https://twitter.com/"), uriStr);
2768 catch (UriFormatException)
2775 sourceText = WebUtility.HtmlDecode(sourceHtml);
2779 return Tuple.Create(sourceText, sourceUri);
2782 public TwitterApiStatus GetInfoApi()
2784 if (Twitter.AccountState != MyCommon.ACCOUNT_STATE.Valid) return null;
2786 if (MyCommon._endingFlag) return null;
2792 res = twCon.RateLimitStatus(ref content);
2796 this.ResetApiStatus();
2800 this.CheckStatusCode(res, content);
2804 MyCommon.TwitterApiInfo.UpdateFromJson(content);
2805 return MyCommon.TwitterApiInfo;
2807 catch (Exception ex)
2809 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
2810 MyCommon.TwitterApiInfo.Reset();
2816 /// ブロック中のユーザーを更新します
2818 /// <exception cref="WebApiException"/>
2819 public void RefreshBlockIds()
2821 if (MyCommon._endingFlag) return;
2824 var newBlockIds = new HashSet<long>();
2827 var ret = this.GetBlockIdsApi(cursor);
2828 newBlockIds.UnionWith(ret.Ids);
2829 cursor = ret.NextCursor;
2830 } while (cursor != 0);
2832 newBlockIds.Remove(this.UserId); // 元のソースにあったので一応残しておく
2834 TabInformations.GetInstance().BlockIds = newBlockIds;
2837 public TwitterIds GetBlockIdsApi(long cursor)
2839 this.CheckAccountState();
2845 res = twCon.GetBlockUserIds(ref content, cursor);
2849 throw new WebApiException("Err:" + e.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", e);
2852 this.CheckStatusCode(res, content);
2856 return TwitterIds.ParseJson(content);
2858 catch(SerializationException e)
2860 var ex = new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, e);
2861 MyCommon.TraceOut(ex);
2866 var ex = new WebApiException("Err:Invalid Json!", content, e);
2867 MyCommon.TraceOut(ex);
2873 /// ミュート中のユーザーIDを更新します
2875 /// <exception cref="WebApiException"/>
2876 public async Task RefreshMuteUserIdsAsync()
2878 if (MyCommon._endingFlag) return;
2880 var ids = await TwitterIds.GetAllItemsAsync(this.GetMuteUserIdsApiAsync)
2881 .ConfigureAwait(false);
2883 TabInformations.GetInstance().MuteUserIds = new HashSet<long>(ids);
2886 public async Task<TwitterIds> GetMuteUserIdsApiAsync(long cursor)
2892 var res = await Task.Run(() => twCon.GetMuteUserIds(ref content, cursor))
2893 .ConfigureAwait(false);
2895 this.CheckStatusCode(res, content);
2897 return TwitterIds.ParseJson(content);
2899 catch (WebException ex)
2901 var ex2 = new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", content, ex);
2902 MyCommon.TraceOut(ex2);
2905 catch (SerializationException ex)
2907 var ex2 = new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
2908 MyCommon.TraceOut(ex2);
2913 public string[] GetHashList()
2918 hashArray = _hashList.ToArray();
2924 public string AccessToken
2928 return twCon.AccessToken;
2932 public string AccessTokenSecret
2936 return twCon.AccessTokenSecret;
2940 private void CheckAccountState()
2942 if (Twitter.AccountState != MyCommon.ACCOUNT_STATE.Valid)
2943 throw new WebApiException("Auth error. Check your account");
2946 private void CheckAccessLevel(TwitterApiAccessLevel accessLevelFlags)
2948 if (!this.AccessLevel.HasFlag(accessLevelFlags))
2949 throw new WebApiException("Auth Err:try to re-authorization.");
2952 private void CheckStatusCode(HttpStatusCode httpStatus, string responseText,
2953 [CallerMemberName] string callerMethodName = "")
2955 if (httpStatus == HttpStatusCode.OK)
2957 Twitter.AccountState = MyCommon.ACCOUNT_STATE.Valid;
2961 if (string.IsNullOrWhiteSpace(responseText))
2963 if (httpStatus == HttpStatusCode.Unauthorized)
2964 Twitter.AccountState = MyCommon.ACCOUNT_STATE.Invalid;
2966 throw new WebApiException("Err:" + httpStatus + "(" + callerMethodName + ")");
2971 var errors = TwitterError.ParseJson(responseText).Errors;
2972 if (errors == null || !errors.Any())
2974 throw new WebApiException("Err:" + httpStatus + "(" + callerMethodName + ")", responseText);
2977 foreach (var error in errors)
2979 if (error.Code == TwitterErrorCode.InvalidToken ||
2980 error.Code == TwitterErrorCode.SuspendedAccount)
2982 Twitter.AccountState = MyCommon.ACCOUNT_STATE.Invalid;
2986 throw new WebApiException("Err:" + string.Join(",", errors.Select(x => x.ToString())) + "(" + callerMethodName + ")", responseText);
2988 catch (SerializationException) { }
2990 throw new WebApiException("Err:" + httpStatus + "(" + callerMethodName + ")", responseText);
2993 public int GetTextLengthRemain(string postText)
2995 var matchDm = Twitter.DMSendTextRegex.Match(postText);
2996 if (matchDm.Success)
2997 return this.GetTextLengthRemainInternal(matchDm.Groups["body"].Value, isDm: true);
2999 return this.GetTextLengthRemainInternal(postText, isDm: false);
3002 private int GetTextLengthRemainInternal(string postText, bool isDm)
3007 while (pos < postText.Length)
3011 if (char.IsSurrogatePair(postText, pos))
3012 pos += 2; // サロゲートペアの場合は2文字分進める
3017 var urls = TweetExtractor.ExtractUrls(postText);
3018 foreach (var url in urls)
3020 var shortUrlLength = url.StartsWith("https://", StringComparison.OrdinalIgnoreCase)
3021 ? this.Configuration.ShortUrlLengthHttps
3022 : this.Configuration.ShortUrlLength;
3024 textLength += shortUrlLength - url.Length;
3028 return this.Configuration.DmTextCharacterLimit - textLength;
3030 return 140 - textLength;
3034 #region "UserStream"
3035 private string trackWord_ = "";
3036 public string TrackWord
3047 private bool allAtReply_ = false;
3048 public bool AllAtReply
3056 allAtReply_ = value;
3060 public event EventHandler NewPostFromStream;
3061 public event EventHandler UserStreamStarted;
3062 public event EventHandler UserStreamStopped;
3063 public event EventHandler<PostDeletedEventArgs> PostDeleted;
3064 public event EventHandler<UserStreamEventReceivedEventArgs> UserStreamEventReceived;
3065 private DateTime _lastUserstreamDataReceived;
3066 private TwitterUserstream userStream;
3068 public class FormattedEvent
3070 public MyCommon.EVENTTYPE Eventtype { get; set; }
3071 public DateTime CreatedAt { get; set; }
3072 public string Event { get; set; }
3073 public string Username { get; set; }
3074 public string Target { get; set; }
3075 public Int64 Id { get; set; }
3076 public bool IsMe { get; set; }
3079 public List<FormattedEvent> storedEvent_ = new List<FormattedEvent>();
3080 public List<FormattedEvent> StoredEvent
3084 return storedEvent_;
3088 storedEvent_ = value;
3092 private readonly IReadOnlyDictionary<string, MyCommon.EVENTTYPE> eventTable = new Dictionary<string, MyCommon.EVENTTYPE>
3094 ["favorite"] = MyCommon.EVENTTYPE.Favorite,
3095 ["unfavorite"] = MyCommon.EVENTTYPE.Unfavorite,
3096 ["follow"] = MyCommon.EVENTTYPE.Follow,
3097 ["list_member_added"] = MyCommon.EVENTTYPE.ListMemberAdded,
3098 ["list_member_removed"] = MyCommon.EVENTTYPE.ListMemberRemoved,
3099 ["block"] = MyCommon.EVENTTYPE.Block,
3100 ["unblock"] = MyCommon.EVENTTYPE.Unblock,
3101 ["user_update"] = MyCommon.EVENTTYPE.UserUpdate,
3102 ["deleted"] = MyCommon.EVENTTYPE.Deleted,
3103 ["list_created"] = MyCommon.EVENTTYPE.ListCreated,
3104 ["list_destroyed"] = MyCommon.EVENTTYPE.ListDestroyed,
3105 ["list_updated"] = MyCommon.EVENTTYPE.ListUpdated,
3106 ["unfollow"] = MyCommon.EVENTTYPE.Unfollow,
3107 ["list_user_subscribed"] = MyCommon.EVENTTYPE.ListUserSubscribed,
3108 ["list_user_unsubscribed"] = MyCommon.EVENTTYPE.ListUserUnsubscribed,
3109 ["mute"] = MyCommon.EVENTTYPE.Mute,
3110 ["unmute"] = MyCommon.EVENTTYPE.Unmute,
3111 ["quoted_tweet"] = MyCommon.EVENTTYPE.QuotedTweet,
3114 public bool IsUserstreamDataReceived
3118 return DateTime.Now.Subtract(this._lastUserstreamDataReceived).TotalSeconds < 31;
3122 private void userStream_StatusArrived(string line)
3124 this._lastUserstreamDataReceived = DateTime.Now;
3125 if (string.IsNullOrEmpty(line)) return;
3127 if (line.First() != '{' || line.Last() != '}')
3129 MyCommon.TraceOut("Invalid JSON (StatusArrived):" + Environment.NewLine + line);
3137 using (var jsonReader = JsonReaderWriterFactory.CreateJsonReader(Encoding.UTF8.GetBytes(line), XmlDictionaryReaderQuotas.Max))
3139 var xElm = XElement.Load(jsonReader);
3140 if (xElm.Element("friends") != null)
3142 Debug.WriteLine("friends");
3145 else if (xElm.Element("delete") != null)
3147 Debug.WriteLine("delete");
3150 if ((idElm = xElm.Element("delete").Element("direct_message")?.Element("id")) != null)
3153 long.TryParse(idElm.Value, out id);
3155 this.PostDeleted?.Invoke(this, new PostDeletedEventArgs(id));
3157 else if ((idElm = xElm.Element("delete").Element("status")?.Element("id")) != null)
3160 long.TryParse(idElm.Value, out id);
3162 this.PostDeleted?.Invoke(this, new PostDeletedEventArgs(id));
3166 MyCommon.TraceOut("delete:" + line);
3169 for (int i = this.StoredEvent.Count - 1; i >= 0; i--)
3171 var sEvt = this.StoredEvent[i];
3172 if (sEvt.Id == id && (sEvt.Event == "favorite" || sEvt.Event == "unfavorite"))
3174 this.StoredEvent.RemoveAt(i);
3179 else if (xElm.Element("limit") != null)
3181 Debug.WriteLine(line);
3184 else if (xElm.Element("event") != null)
3186 Debug.WriteLine("event: " + xElm.Element("event").Value);
3187 CreateEventFromJson(line);
3190 else if (xElm.Element("direct_message") != null)
3192 Debug.WriteLine("direct_message");
3195 else if (xElm.Element("retweeted_status") != null)
3197 var sourceUserId = xElm.XPathSelectElement("/user/id_str").Value;
3198 var targetUserId = xElm.XPathSelectElement("/retweeted_status/user/id_str").Value;
3200 // 自分に関係しないリツイートの場合は無視する
3201 var selfUserId = this.UserId.ToString();
3202 if (sourceUserId == selfUserId || targetUserId == selfUserId)
3204 // 公式 RT をイベントとしても扱う
3205 var evt = CreateEventFromRetweet(xElm);
3208 this.StoredEvent.Insert(0, evt);
3210 this.UserStreamEventReceived?.Invoke(this, new UserStreamEventReceivedEventArgs(evt));
3214 // 従来通り公式 RT の表示も行うため return しない
3216 else if (xElm.Element("scrub_geo") != null)
3220 TabInformations.GetInstance().ScrubGeoReserve(long.Parse(xElm.Element("scrub_geo").Element("user_id").Value),
3221 long.Parse(xElm.Element("scrub_geo").Element("up_to_status_id").Value));
3225 MyCommon.TraceOut("scrub_geo:" + line);
3233 CreateDirectMessagesFromJson(line, MyCommon.WORKERTYPE.UserStream, false);
3237 CreatePostsFromJson("[" + line + "]", MyCommon.WORKERTYPE.Timeline, null, false);
3240 catch (WebApiException ex)
3242 MyCommon.TraceOut(ex);
3245 catch(NullReferenceException)
3247 MyCommon.TraceOut("NullRef StatusArrived: " + line);
3250 this.NewPostFromStream?.Invoke(this, EventArgs.Empty);
3254 /// UserStreamsから受信した公式RTをイベントに変換します
3256 private FormattedEvent CreateEventFromRetweet(XElement xElm)
3258 return new FormattedEvent
3260 Eventtype = MyCommon.EVENTTYPE.Retweet,
3262 CreatedAt = MyCommon.DateTimeParse(xElm.XPathSelectElement("/created_at").Value),
3263 IsMe = xElm.XPathSelectElement("/user/id_str").Value == this.UserId.ToString(),
3264 Username = xElm.XPathSelectElement("/user/screen_name").Value,
3265 Target = string.Format("@{0}:{1}", new[]
3267 xElm.XPathSelectElement("/retweeted_status/user/screen_name").Value,
3268 WebUtility.HtmlDecode(xElm.XPathSelectElement("/retweeted_status/text").Value),
3270 Id = long.Parse(xElm.XPathSelectElement("/retweeted_status/id_str").Value),
3274 private void CreateEventFromJson(string content)
3276 TwitterStreamEvent eventData = null;
3279 eventData = TwitterStreamEvent.ParseJson(content);
3281 catch(SerializationException ex)
3283 MyCommon.TraceOut(ex, "Event Serialize Exception!" + Environment.NewLine + content);
3287 MyCommon.TraceOut(ex, "Event Exception!" + Environment.NewLine + content);
3290 var evt = new FormattedEvent();
3291 evt.CreatedAt = MyCommon.DateTimeParse(eventData.CreatedAt);
3292 evt.Event = eventData.Event;
3293 evt.Username = eventData.Source.ScreenName;
3294 evt.IsMe = evt.Username.ToLower().Equals(this.Username.ToLower());
3296 MyCommon.EVENTTYPE eventType;
3297 eventTable.TryGetValue(eventData.Event, out eventType);
3298 evt.Eventtype = eventType;
3300 TwitterStreamEvent<TwitterStatus> tweetEvent;
3302 switch (eventData.Event)
3304 case "access_revoked":
3305 case "access_unrevoked":
3307 case "user_suspend":
3310 if (eventData.Target.ScreenName.ToLower().Equals(_uname))
3312 if (!this.followerId.Contains(eventData.Source.Id)) this.followerId.Add(eventData.Source.Id);
3316 return; //Block後のUndoをすると、SourceとTargetが逆転したfollowイベントが帰ってくるため。
3321 evt.Target = "@" + eventData.Target.ScreenName;
3323 case "favorited_retweet":
3324 case "retweeted_retweet":
3328 tweetEvent = TwitterStreamEvent<TwitterStatus>.ParseJson(content);
3329 evt.Target = "@" + tweetEvent.TargetObject.User.ScreenName + ":" + WebUtility.HtmlDecode(tweetEvent.TargetObject.Text);
3330 evt.Id = tweetEvent.TargetObject.Id;
3332 if (SettingCommon.Instance.IsRemoveSameEvent)
3334 if (this.StoredEvent.Any(ev => ev.Username == evt.Username && ev.Eventtype == evt.Eventtype && ev.Target == evt.Target))
3338 var tabinfo = TabInformations.GetInstance();
3341 var statusId = tweetEvent.TargetObject.Id;
3342 if (!tabinfo.Posts.TryGetValue(statusId, out post))
3345 if (eventData.Event == "favorite")
3347 var favTab = tabinfo.GetTabByType(MyCommon.TabUsageType.Favorites);
3348 if (!favTab.Contains(post.StatusId))
3349 favTab.AddPostImmediately(post.StatusId, post.IsRead);
3351 if (tweetEvent.Source.Id == this.UserId)
3355 else if (tweetEvent.Target.Id == this.UserId)
3357 post.FavoritedCount++;
3359 if (SettingCommon.Instance.FavEventUnread)
3360 tabinfo.SetReadAllTab(post.StatusId, read: false);
3365 if (tweetEvent.Source.Id == this.UserId)
3369 else if (tweetEvent.Target.Id == this.UserId)
3371 post.FavoritedCount = Math.Max(0, post.FavoritedCount - 1);
3375 case "quoted_tweet":
3376 if (evt.IsMe) return;
3378 tweetEvent = TwitterStreamEvent<TwitterStatus>.ParseJson(content);
3379 evt.Target = "@" + tweetEvent.TargetObject.User.ScreenName + ":" + WebUtility.HtmlDecode(tweetEvent.TargetObject.Text);
3380 evt.Id = tweetEvent.TargetObject.Id;
3382 if (SettingCommon.Instance.IsRemoveSameEvent)
3384 if (this.StoredEvent.Any(ev => ev.Username == evt.Username && ev.Eventtype == evt.Eventtype && ev.Target == evt.Target))
3388 case "list_member_added":
3389 case "list_member_removed":
3390 case "list_created":
3391 case "list_destroyed":
3392 case "list_updated":
3393 case "list_user_subscribed":
3394 case "list_user_unsubscribed":
3395 var listEvent = TwitterStreamEvent<TwitterList>.ParseJson(content);
3396 evt.Target = listEvent.TargetObject.FullName;
3399 if (!TabInformations.GetInstance().BlockIds.Contains(eventData.Target.Id)) TabInformations.GetInstance().BlockIds.Add(eventData.Target.Id);
3403 if (TabInformations.GetInstance().BlockIds.Contains(eventData.Target.Id)) TabInformations.GetInstance().BlockIds.Remove(eventData.Target.Id);
3412 evt.Target = "@" + eventData.Target.ScreenName;
3413 if (!TabInformations.GetInstance().MuteUserIds.Contains(eventData.Target.Id))
3415 TabInformations.GetInstance().MuteUserIds.Add(eventData.Target.Id);
3419 evt.Target = "@" + eventData.Target.ScreenName;
3420 if (TabInformations.GetInstance().MuteUserIds.Contains(eventData.Target.Id))
3422 TabInformations.GetInstance().MuteUserIds.Remove(eventData.Target.Id);
3427 MyCommon.TraceOut("Unknown Event:" + evt.Event + Environment.NewLine + content);
3430 this.StoredEvent.Insert(0, evt);
3432 this.UserStreamEventReceived?.Invoke(this, new UserStreamEventReceivedEventArgs(evt));
3435 private void userStream_Started()
3437 this.UserStreamStarted?.Invoke(this, EventArgs.Empty);
3440 private void userStream_Stopped()
3442 this.UserStreamStopped?.Invoke(this, EventArgs.Empty);
3445 public bool UserStreamEnabled
3449 return userStream == null ? false : userStream.Enabled;
3453 public void StartUserStream()
3455 if (userStream != null)
3459 userStream = new TwitterUserstream(twCon);
3460 userStream.StatusArrived += userStream_StatusArrived;
3461 userStream.Started += userStream_Started;
3462 userStream.Stopped += userStream_Stopped;
3463 userStream.Start(this.AllAtReply, this.TrackWord);
3466 public void StopUserStream()
3468 userStream?.Dispose();
3470 if (!MyCommon._endingFlag)
3472 this.UserStreamStopped?.Invoke(this, EventArgs.Empty);
3476 public void ReconnectUserStream()
3478 if (userStream != null)
3480 this.StartUserStream();
3484 private class TwitterUserstream : IDisposable
3486 public event Action<string> StatusArrived;
3487 public event Action Stopped;
3488 public event Action Started;
3489 private HttpTwitter twCon;
3491 private Thread _streamThread;
3492 private bool _streamActive;
3494 private bool _allAtreplies = false;
3495 private string _trackwords = "";
3497 public TwitterUserstream(HttpTwitter twitterConnection)
3499 twCon = (HttpTwitter)twitterConnection.Clone();
3502 public void Start(bool allAtReplies, string trackwords)
3504 this.AllAtReplies = allAtReplies;
3505 this.TrackWords = trackwords;
3506 _streamActive = true;
3507 if (_streamThread != null && _streamThread.IsAlive) return;
3508 _streamThread = new Thread(UserStreamLoop);
3509 _streamThread.Name = "UserStreamReceiver";
3510 _streamThread.IsBackground = true;
3511 _streamThread.Start();
3518 return _streamActive;
3522 public bool AllAtReplies
3526 return _allAtreplies;
3530 _allAtreplies = value;
3534 public string TrackWords
3542 _trackwords = value;
3546 private void UserStreamLoop()
3552 StreamReader sr = null;
3555 if (!MyCommon.IsNetworkAvailable())
3563 var res = twCon.UserStream(ref st, _allAtreplies, _trackwords, Networking.GetUserAgentString());
3567 case HttpStatusCode.OK:
3568 Twitter.AccountState = MyCommon.ACCOUNT_STATE.Valid;
3570 case HttpStatusCode.Unauthorized:
3571 Twitter.AccountState = MyCommon.ACCOUNT_STATE.Invalid;
3579 //MyCommon.TraceOut("Stop:stream is null")
3583 sr = new StreamReader(st);
3585 while (_streamActive && !sr.EndOfStream && Twitter.AccountState == MyCommon.ACCOUNT_STATE.Valid)
3587 StatusArrived?.Invoke(sr.ReadLine());
3588 //this.LastTime = Now;
3591 if (sr.EndOfStream || Twitter.AccountState == MyCommon.ACCOUNT_STATE.Invalid)
3594 //MyCommon.TraceOut("Stop:EndOfStream")
3599 catch(WebException ex)
3601 if (ex.Status == WebExceptionStatus.Timeout)
3603 sleepSec = 30; //MyCommon.TraceOut("Stop:Timeout")
3605 else if (ex.Response != null && (int)((HttpWebResponse)ex.Response).StatusCode == 420)
3607 //MyCommon.TraceOut("Stop:Connection Limit")
3613 //MyCommon.TraceOut("Stop:WebException " + ex.Status.ToString())
3616 catch(ThreadAbortException)
3623 //MyCommon.TraceOut("Stop:IOException with Active." + Environment.NewLine + ex.Message)
3625 catch(ArgumentException ex)
3627 //System.ArgumentException: ストリームを読み取れませんでした。
3628 //サーバー側もしくは通信経路上で切断された場合?タイムアウト頻発後発生
3630 MyCommon.TraceOut(ex, "Stop:ArgumentException");
3634 MyCommon.TraceOut("Stop:Exception." + Environment.NewLine + ex.Message);
3635 MyCommon.ExceptionOut(ex);
3644 twCon.RequestAbort();
3649 while (_streamActive && ms < sleepSec * 1000)
3657 } while (this._streamActive);
3663 MyCommon.TraceOut("Stop:EndLoop");
3666 #region "IDisposable Support"
3667 private bool disposedValue; // 重複する呼び出しを検出するには
3670 protected virtual void Dispose(bool disposing)
3672 if (!this.disposedValue)
3676 _streamActive = false;
3677 if (_streamThread != null && _streamThread.IsAlive)
3679 _streamThread.Abort();
3683 this.disposedValue = true;
3686 //protected Overrides void Finalize()
3688 // // このコードを変更しないでください。クリーンアップ コードを上の Dispose(bool disposing) に記述します。
3690 // MyBase.Finalize()
3693 // このコードは、破棄可能なパターンを正しく実装できるように Visual Basic によって追加されました。
3694 public void Dispose()
3696 // このコードを変更しないでください。クリーンアップ コードを上の Dispose(bool disposing) に記述します。
3698 GC.SuppressFinalize(this);
3705 #region "IDisposable Support"
3706 private bool disposedValue; // 重複する呼び出しを検出するには
3709 protected virtual void Dispose(bool disposing)
3711 if (!this.disposedValue)
3715 this.StopUserStream();
3718 this.disposedValue = true;
3721 //protected Overrides void Finalize()
3723 // // このコードを変更しないでください。クリーンアップ コードを上の Dispose(bool disposing) に記述します。
3725 // MyBase.Finalize()
3728 // このコードは、破棄可能なパターンを正しく実装できるように Visual Basic によって追加されました。
3729 public void Dispose()
3731 // このコードを変更しないでください。クリーンアップ コードを上の Dispose(bool disposing) に記述します。
3733 GC.SuppressFinalize(this);
3738 public class PostDeletedEventArgs : EventArgs
3740 public long StatusId { get; }
3742 public PostDeletedEventArgs(long statusId)
3744 this.StatusId = statusId;
3748 public class UserStreamEventReceivedEventArgs : EventArgs
3750 public Twitter.FormattedEvent EventData { get; }
3752 public UserStreamEventReceivedEventArgs(Twitter.FormattedEvent eventData)
3754 this.EventData = eventData;