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();
1640 var source = ParseSource(sourceHtml);
1641 post.Source = source.Item1;
1642 post.SourceUri = source.Item2;
1644 post.IsReply = post.ReplyToList.Contains(_uname);
1645 post.IsExcludeReply = false;
1653 if (followerId.Count > 0) post.IsOwl = !followerId.Contains(post.UserId);
1661 /// ツイートに含まれる引用ツイートのURLからステータスIDを抽出
1663 public static IEnumerable<long> GetQuoteTweetStatusIds(IEnumerable<TwitterEntity> entities)
1665 foreach (var entity in entities)
1667 var entityUrl = entity as TwitterEntityUrl;
1668 if (entityUrl == null)
1671 var match = Twitter.StatusUrlRegex.Match(entityUrl.ExpandedUrl);
1674 yield return long.Parse(match.Groups["StatusId"].Value);
1679 private long? CreatePostsFromJson(string content, MyCommon.WORKERTYPE gType, TabClass tab, bool read)
1681 TwitterStatus[] items;
1684 items = TwitterStatus.ParseJsonArray(content);
1686 catch(SerializationException ex)
1688 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
1689 throw new WebApiException("Json Parse Error(DataContractJsonSerializer)", content, ex);
1693 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
1694 throw new WebApiException("Invalid Json!", content, ex);
1697 long? minimumId = null;
1699 foreach (var status in items)
1701 PostClass post = null;
1702 post = CreatePostsFromStatusData(status);
1703 if (post == null) continue;
1705 if (minimumId == null || minimumId.Value > post.StatusId)
1706 minimumId = post.StatusId;
1713 if (TabInformations.GetInstance().ContainsKey(post.StatusId)) continue;
1717 if (tab.Contains(post.StatusId)) continue;
1722 if (gType != MyCommon.WORKERTYPE.UserTimeline &&
1723 post.RetweetedByUserId != null && this.noRTId.Contains(post.RetweetedByUserId.Value)) continue;
1726 if (post.IsMe && !read && _readOwnPost) post.IsRead = true;
1728 //非同期アイコン取得&StatusDictionaryに追加
1729 if (tab != null && tab.IsInnerStorageTabType)
1730 tab.AddPostToInnerStorage(post);
1732 TabInformations.GetInstance().AddPost(post);
1738 private long? CreatePostsFromSearchJson(string content, TabClass tab, bool read, int count, bool more)
1740 TwitterSearchResult items;
1743 items = TwitterSearchResult.ParseJson(content);
1745 catch (SerializationException ex)
1747 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
1748 throw new WebApiException("Json Parse Error(DataContractJsonSerializer)", content, ex);
1750 catch (Exception ex)
1752 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
1753 throw new WebApiException("Invalid Json!", content, ex);
1756 long? minimumId = null;
1758 foreach (var result in items.Statuses)
1760 PostClass post = null;
1761 post = CreatePostsFromStatusData(result);
1765 // Search API は相変わらずぶっ壊れたデータを返すことがあるため、必要なデータが欠如しているものは取得し直す
1768 post = this.GetStatusApi(read, result.Id);
1770 catch (WebApiException)
1776 if (minimumId == null || minimumId.Value > post.StatusId)
1777 minimumId = post.StatusId;
1779 if (!more && post.StatusId > tab.SinceId) tab.SinceId = post.StatusId;
1785 if (TabInformations.GetInstance().ContainsKey(post.StatusId)) continue;
1789 if (tab.Contains(post.StatusId)) continue;
1794 if ((post.IsMe && !read) && this._readOwnPost) post.IsRead = true;
1796 //非同期アイコン取得&StatusDictionaryに追加
1797 if (tab != null && tab.IsInnerStorageTabType)
1798 tab.AddPostToInnerStorage(post);
1800 TabInformations.GetInstance().AddPost(post);
1806 private void CreateFavoritePostsFromJson(string content, bool read)
1808 TwitterStatus[] item;
1811 item = TwitterStatus.ParseJsonArray(content);
1813 catch (SerializationException ex)
1815 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
1816 throw new WebApiException("Json Parse Error(DataContractJsonSerializer)", content, ex);
1818 catch (Exception ex)
1820 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
1821 throw new WebApiException("Invalid Json!", content, ex);
1824 var favTab = TabInformations.GetInstance().GetTabByType(MyCommon.TabUsageType.Favorites);
1826 foreach (var status in item)
1831 if (favTab.Contains(status.Id)) continue;
1834 var post = CreatePostsFromStatusData(status, true);
1835 if (post == null) continue;
1839 TabInformations.GetInstance().AddPost(post);
1843 public void GetListStatus(bool read,
1850 var count = GetApiResultCount(MyCommon.WORKERTYPE.List, more, startup);
1856 res = twCon.GetListsStatuses(tab.ListInfo.UserId, tab.ListInfo.Id, count, tab.OldestId, null, SettingCommon.Instance.IsListsIncludeRts, ref content);
1860 res = twCon.GetListsStatuses(tab.ListInfo.UserId, tab.ListInfo.Id, count, null, null, SettingCommon.Instance.IsListsIncludeRts, ref content);
1865 throw new WebApiException("Err:" + ex.Message, ex);
1868 this.CheckStatusCode(res, content);
1870 var minimumId = CreatePostsFromJson(content, MyCommon.WORKERTYPE.List, tab, read);
1872 if (minimumId != null)
1873 tab.OldestId = minimumId.Value;
1877 /// startStatusId からリプライ先の発言を辿る。発言は posts 以外からは検索しない。
1879 /// <returns>posts の中から検索されたリプライチェインの末端</returns>
1880 internal static PostClass FindTopOfReplyChain(IDictionary<Int64, PostClass> posts, Int64 startStatusId)
1882 if (!posts.ContainsKey(startStatusId))
1883 throw new ArgumentException("startStatusId (" + startStatusId + ") が posts の中から見つかりませんでした。", nameof(startStatusId));
1885 var nextPost = posts[startStatusId];
1886 while (nextPost.InReplyToStatusId != null)
1888 if (!posts.ContainsKey(nextPost.InReplyToStatusId.Value))
1890 nextPost = posts[nextPost.InReplyToStatusId.Value];
1896 public void GetRelatedResult(bool read, TabClass tab)
1898 var relPosts = new Dictionary<Int64, PostClass>();
1899 if (tab.RelationTargetPost.TextFromApi.Contains("@") && tab.RelationTargetPost.InReplyToStatusId == null)
1902 var p = TabInformations.GetInstance()[tab.RelationTargetPost.StatusId];
1903 if (p != null && p.InReplyToStatusId != null)
1905 tab.RelationTargetPost = p;
1909 p = this.GetStatusApi(read, tab.RelationTargetPost.StatusId);
1910 tab.RelationTargetPost = p;
1913 relPosts.Add(tab.RelationTargetPost.StatusId, tab.RelationTargetPost);
1915 Exception lastException = null;
1917 // in_reply_to_status_id を使用してリプライチェインを辿る
1918 var nextPost = FindTopOfReplyChain(relPosts, tab.RelationTargetPost.StatusId);
1920 while (nextPost.InReplyToStatusId != null && loopCount++ <= 20)
1922 var inReplyToId = nextPost.InReplyToStatusId.Value;
1924 var inReplyToPost = TabInformations.GetInstance()[inReplyToId];
1925 if (inReplyToPost == null)
1929 inReplyToPost = this.GetStatusApi(read, inReplyToId);
1931 catch (WebApiException ex)
1938 relPosts.Add(inReplyToPost.StatusId, inReplyToPost);
1940 nextPost = FindTopOfReplyChain(relPosts, nextPost.StatusId);
1943 //MRTとかに対応のためツイート内にあるツイートを指すURLを取り込む
1944 var text = tab.RelationTargetPost.Text;
1945 var ma = Twitter.StatusUrlRegex.Matches(text).Cast<Match>()
1946 .Concat(Twitter.ThirdPartyStatusUrlRegex.Matches(text).Cast<Match>());
1947 foreach (var _match in ma)
1950 if (Int64.TryParse(_match.Groups["StatusId"].Value, out _statusId))
1952 if (relPosts.ContainsKey(_statusId))
1955 var p = TabInformations.GetInstance()[_statusId];
1960 p = this.GetStatusApi(read, _statusId);
1962 catch (WebApiException ex)
1970 relPosts.Add(p.StatusId, p);
1974 relPosts.Values.ToList().ForEach(p =>
1976 if (p.IsMe && !read && this._readOwnPost)
1981 tab.AddPostToInnerStorage(p);
1984 if (lastException != null)
1985 throw new WebApiException(lastException.Message, lastException);
1988 public void GetSearch(bool read,
1994 var count = GetApiResultCount(MyCommon.WORKERTYPE.PublicSearch, more, false);
1996 long? sinceId = null;
1999 maxId = tab.OldestId - 1;
2003 sinceId = tab.SinceId;
2008 // TODO:一時的に40>100件に 件数変更UI作成の必要あり
2009 res = twCon.Search(tab.SearchWords, tab.SearchLang, count, maxId, sinceId, ref content);
2013 throw new WebApiException("Err:" + ex.Message, ex);
2017 case HttpStatusCode.BadRequest:
2018 throw new WebApiException("Invalid query", content);
2019 case HttpStatusCode.NotFound:
2020 throw new WebApiException("Invalid query", content);
2021 case HttpStatusCode.PaymentRequired: //API Documentには420と書いてあるが、該当コードがないので402にしてある
2022 throw new WebApiException("Search API Limit?", content);
2023 case HttpStatusCode.OK:
2026 throw new WebApiException("Err:" + res.ToString() + "(" + MethodBase.GetCurrentMethod().Name + ")", content);
2029 if (!TabInformations.GetInstance().ContainsTab(tab))
2032 var minimumId = this.CreatePostsFromSearchJson(content, tab, read, count, more);
2034 if (minimumId != null)
2035 tab.OldestId = minimumId.Value;
2038 private void CreateDirectMessagesFromJson(string content, MyCommon.WORKERTYPE gType, bool read)
2040 TwitterDirectMessage[] item;
2043 if (gType == MyCommon.WORKERTYPE.UserStream)
2045 item = new[] { TwitterStreamEventDirectMessage.ParseJson(content).DirectMessage };
2049 item = TwitterDirectMessage.ParseJsonArray(content);
2052 catch(SerializationException ex)
2054 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
2055 throw new WebApiException("Json Parse Error(DataContractJsonSerializer)", content, ex);
2059 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
2060 throw new WebApiException("Invalid Json!", content, ex);
2063 foreach (var message in item)
2065 var post = new PostClass();
2068 post.StatusId = message.Id;
2069 if (gType != MyCommon.WORKERTYPE.UserStream)
2071 if (gType == MyCommon.WORKERTYPE.DirectMessegeRcv)
2073 if (minDirectmessage > post.StatusId) minDirectmessage = post.StatusId;
2077 if (minDirectmessageSent > post.StatusId) minDirectmessageSent = post.StatusId;
2084 if (TabInformations.GetInstance().GetTabByType(MyCommon.TabUsageType.DirectMessage).Contains(post.StatusId)) continue;
2088 post.CreatedAt = MyCommon.DateTimeParse(message.CreatedAt);
2090 var textFromApi = message.Text;
2092 post.Text = CreateHtmlAnchor(textFromApi, post.ReplyToList, message.Entities, post.Media);
2093 post.TextFromApi = this.ReplaceTextFromApi(textFromApi, message.Entities);
2094 post.TextFromApi = WebUtility.HtmlDecode(post.TextFromApi);
2095 post.TextFromApi = post.TextFromApi.Replace("<3", "\u2661");
2098 post.QuoteStatusIds = GetQuoteTweetStatusIds(message.Entities).Distinct().ToArray();
2102 if (gType == MyCommon.WORKERTYPE.UserStream)
2104 if (twCon.AuthenticatedUsername.Equals(message.Recipient.ScreenName, StringComparison.CurrentCultureIgnoreCase))
2106 user = message.Sender;
2112 user = message.Recipient;
2119 if (gType == MyCommon.WORKERTYPE.DirectMessegeRcv)
2121 user = message.Sender;
2127 user = message.Recipient;
2133 post.UserId = user.Id;
2134 post.ScreenName = user.ScreenName;
2135 post.Nickname = user.Name.Trim();
2136 post.ImageUrl = user.ProfileImageUrlHttps;
2137 post.IsProtect = user.Protected;
2141 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
2142 MessageBox.Show("Parse Error(CreateDirectMessagesFromJson)");
2147 if (post.IsMe && !read && _readOwnPost) post.IsRead = true;
2148 post.IsReply = false;
2149 post.IsExcludeReply = false;
2152 var dmTab = TabInformations.GetInstance().GetTabByType(MyCommon.TabUsageType.DirectMessage);
2153 dmTab.AddPostToInnerStorage(post);
2157 public void GetDirectMessageApi(bool read,
2158 MyCommon.WORKERTYPE gType,
2161 this.CheckAccountState();
2162 this.CheckAccessLevel(TwitterApiAccessLevel.ReadWriteAndDirectMessage);
2166 var count = GetApiResultCount(gType, more, false);
2170 if (gType == MyCommon.WORKERTYPE.DirectMessegeRcv)
2174 res = twCon.DirectMessages(count, minDirectmessage, null, ref content);
2178 res = twCon.DirectMessages(count, null, null, ref content);
2185 res = twCon.DirectMessagesSent(count, minDirectmessageSent, null, ref content);
2189 res = twCon.DirectMessagesSent(count, null, null, ref content);
2195 throw new WebApiException("Err:" + ex.Message, ex);
2198 this.CheckStatusCode(res, content);
2200 CreateDirectMessagesFromJson(content, gType, read);
2203 public void GetFavoritesApi(bool read,
2206 this.CheckAccountState();
2210 var count = GetApiResultCount(MyCommon.WORKERTYPE.Favorites, more, false);
2214 res = twCon.Favorites(count, ref content);
2218 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
2221 this.CheckStatusCode(res, content);
2223 CreateFavoritePostsFromJson(content, read);
2226 private string ReplaceTextFromApi(string text, TwitterEntities entities)
2228 if (entities != null)
2230 if (entities.Urls != null)
2232 foreach (var m in entities.Urls)
2234 if (!string.IsNullOrEmpty(m.DisplayUrl)) text = text.Replace(m.Url, m.DisplayUrl);
2237 if (entities.Media != null)
2239 foreach (var m in entities.Media)
2241 if (!string.IsNullOrEmpty(m.DisplayUrl)) text = text.Replace(m.Url, m.DisplayUrl);
2251 /// <exception cref="WebApiException"/>
2252 public void RefreshFollowerIds()
2254 if (MyCommon._endingFlag) return;
2257 var newFollowerIds = new HashSet<long>();
2260 var ret = this.GetFollowerIdsApi(ref cursor);
2261 newFollowerIds.UnionWith(ret.Ids);
2262 cursor = ret.NextCursor;
2263 } while (cursor != 0);
2265 this.followerId = newFollowerIds;
2266 TabInformations.GetInstance().RefreshOwl(this.followerId);
2268 this._GetFollowerResult = true;
2271 public bool GetFollowersSuccess
2275 return _GetFollowerResult;
2279 private TwitterIds GetFollowerIdsApi(ref long cursor)
2281 this.CheckAccountState();
2287 res = twCon.FollowerIds(cursor, ref content);
2291 throw new WebApiException("Err:" + e.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", e);
2294 this.CheckStatusCode(res, content);
2298 var ret = TwitterIds.ParseJson(content);
2300 if (ret.Ids == null)
2302 var ex = new WebApiException("Err: ret.id == null (GetFollowerIdsApi)", content);
2303 MyCommon.ExceptionOut(ex);
2309 catch(SerializationException e)
2311 var ex = new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, e);
2312 MyCommon.TraceOut(ex);
2317 var ex = new WebApiException("Err:Invalid Json!", content, e);
2318 MyCommon.TraceOut(ex);
2324 /// RT 非表示ユーザーを更新します
2326 /// <exception cref="WebApiException"/>
2327 public void RefreshNoRetweetIds()
2329 if (MyCommon._endingFlag) return;
2331 this.noRTId = this.NoRetweetIdsApi();
2333 this._GetNoRetweetResult = true;
2336 private long[] NoRetweetIdsApi()
2338 this.CheckAccountState();
2344 res = twCon.NoRetweetIds(ref content);
2348 throw new WebApiException("Err:" + e.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", e);
2351 this.CheckStatusCode(res, content);
2355 return MyCommon.CreateDataFromJson<long[]>(content);
2357 catch(SerializationException e)
2359 var ex = new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, e);
2360 MyCommon.TraceOut(ex);
2365 var ex = new WebApiException("Err:Invalid Json!", content, e);
2366 MyCommon.TraceOut(ex);
2371 public bool GetNoRetweetSuccess
2375 return _GetNoRetweetResult;
2380 /// t.co の文字列長などの設定情報を更新します
2382 /// <exception cref="WebApiException"/>
2383 public void RefreshConfiguration()
2385 this.Configuration = this.ConfigurationApi();
2388 private TwitterConfiguration ConfigurationApi()
2394 res = twCon.GetConfiguration(ref content);
2398 throw new WebApiException("Err:" + e.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", e);
2401 this.CheckStatusCode(res, content);
2405 return TwitterConfiguration.ParseJson(content);
2407 catch(SerializationException e)
2409 var ex = new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, e);
2410 MyCommon.TraceOut(ex);
2415 var ex = new WebApiException("Err:Invalid Json!", content, e);
2416 MyCommon.TraceOut(ex);
2421 public void GetListsApi()
2423 this.CheckAccountState();
2426 IEnumerable<ListElement> lists;
2431 res = twCon.GetLists(this.Username, ref content);
2433 catch (Exception ex)
2435 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
2438 this.CheckStatusCode(res, content);
2442 lists = TwitterList.ParseJsonArray(content)
2443 .Select(x => new ListElement(x, this));
2445 catch (SerializationException ex)
2447 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
2448 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
2450 catch (Exception ex)
2452 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
2453 throw new WebApiException("Err:Invalid Json!", content, ex);
2458 res = twCon.GetListsSubscriptions(this.Username, ref content);
2460 catch (Exception ex)
2462 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
2465 this.CheckStatusCode(res, content);
2469 lists = lists.Concat(TwitterList.ParseJsonArray(content)
2470 .Select(x => new ListElement(x, this)));
2472 catch (SerializationException ex)
2474 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
2475 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
2477 catch (Exception ex)
2479 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
2480 throw new WebApiException("Err:Invalid Json!", content, ex);
2483 TabInformations.GetInstance().SubscribableLists = lists.ToList();
2486 public void DeleteList(string list_id)
2493 res = twCon.DeleteListID(this.Username, list_id, ref content);
2497 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
2500 this.CheckStatusCode(res, content);
2503 public ListElement EditList(string list_id, string new_name, bool isPrivate, string description)
2510 res = twCon.UpdateListID(this.Username, list_id, new_name, isPrivate, description, ref content);
2514 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
2517 this.CheckStatusCode(res, content);
2521 var le = TwitterList.ParseJson(content);
2522 return new ListElement(le, this);
2524 catch(SerializationException ex)
2526 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
2527 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
2531 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
2532 throw new WebApiException("Err:Invalid Json!", content, ex);
2536 public long GetListMembers(string list_id, List<UserInfo> lists, long cursor)
2538 this.CheckAccountState();
2544 res = twCon.GetListMembers(this.Username, list_id, cursor, ref content);
2548 throw new WebApiException("Err:" + ex.Message);
2551 this.CheckStatusCode(res, content);
2555 var users = TwitterUsers.ParseJson(content);
2556 Array.ForEach<TwitterUser>(
2558 u => lists.Add(new UserInfo(u)));
2560 return users.NextCursor;
2562 catch(SerializationException ex)
2564 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
2565 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
2569 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
2570 throw new WebApiException("Err:Invalid Json!", content, ex);
2574 public void CreateListApi(string listName, bool isPrivate, string description)
2576 this.CheckAccountState();
2582 res = twCon.CreateLists(listName, isPrivate, description, ref content);
2586 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
2589 this.CheckStatusCode(res, content);
2593 var le = TwitterList.ParseJson(content);
2594 TabInformations.GetInstance().SubscribableLists.Add(new ListElement(le, this));
2596 catch(SerializationException ex)
2598 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
2599 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
2603 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
2604 throw new WebApiException("Err:Invalid Json!", content, ex);
2608 public bool ContainsUserAtList(string listId, string user)
2610 this.CheckAccountState();
2617 res = this.twCon.ShowListMember(listId, user, ref content);
2621 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
2624 if (res == HttpStatusCode.NotFound)
2629 this.CheckStatusCode(res, content);
2633 TwitterUser.ParseJson(content);
2642 public void AddUserToList(string listId, string user)
2649 res = twCon.CreateListMembers(listId, user, ref content);
2653 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
2656 this.CheckStatusCode(res, content);
2659 public void RemoveUserToList(string listId, string user)
2666 res = twCon.DeleteListMembers(listId, user, ref content);
2670 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
2673 this.CheckStatusCode(res, content);
2678 public int fromIndex { get; set; }
2679 public int toIndex { get; set; }
2680 public range(int fromIndex, int toIndex)
2682 this.fromIndex = fromIndex;
2683 this.toIndex = toIndex;
2686 public async Task<string> CreateHtmlAnchorAsync(string Text, List<string> AtList, Dictionary<string, string> media)
2688 if (Text == null) return null;
2689 var retStr = Text.Replace(">", "<<<<<tweenだいなり>>>>>").Replace("<", "<<<<<tweenしょうなり>>>>>");
2691 //const string url_valid_domain = "(?<domain>(?:[^\p{P}\s][\.\-_](?=[^\p{P}\s])|[^\p{P}\s]){1,}\.[a-z]{2,}(?::[0-9]+)?)"
2692 //const string url_valid_general_path_chars = "[a-z0-9!*';:=+$/%#\[\]\-_,~]"
2693 //const string url_balance_parens = "(?:\(" + url_valid_general_path_chars + "+\))"
2694 //const string url_valid_url_path_ending_chars = "(?:[a-z0-9=_#/\-\+]+|" + url_balance_parens + ")"
2695 //const string pth = "(?:" + url_balance_parens +
2696 // "|@" + url_valid_general_path_chars + "+/" +
2697 // "|[.,]?" + url_valid_general_path_chars + "+" +
2699 //const string pth2 = "(/(?:" +
2700 // pth + "+" + url_valid_url_path_ending_chars + "|" +
2701 // pth + "+" + url_valid_url_path_ending_chars + "?|" +
2702 // url_valid_url_path_ending_chars +
2704 //const string qry = "(?<query>\?[a-z0-9!*'();:&=+$/%#\[\]\-_.,~]*[a-z0-9_&=#])?"
2705 //const string rgUrl = "(?<before>(?:[^\""':!=#]|^|\:/))" +
2706 // "(?<url>(?<protocol>https?://)" +
2707 // url_valid_domain +
2711 //const string rgUrl = "(?<before>(?:[^\""':!=#]|^|\:/))" +
2712 // "(?<url>(?<protocol>https?://|www\.)" +
2713 // url_valid_domain +
2718 retStr = await new Regex(rgUrl, RegexOptions.IgnoreCase).ReplaceAsync(retStr, async mu =>
2720 var sb = new StringBuilder(mu.Result("${before}<a href=\""));
2721 //if (mu.Result("${protocol}").StartsWith("w", StringComparison.OrdinalIgnoreCase))
2722 // sb.Append("http://");
2724 var url = mu.Result("${url}");
2725 var title = await ShortUrl.Instance.ExpandUrlAsync(url);
2726 sb.Append(url + "\" title=\"" + MyCommon.ConvertToReadableUrl(title) + "\">").Append(url).Append("</a>");
2727 if (media != null && !media.ContainsKey(url)) media.Add(url, title);
2728 return sb.ToString();
2732 retStr = Regex.Replace(retStr,
2733 @"(^|[^a-zA-Z0-9_/])([@@]+)([a-zA-Z0-9_]{1,20}/[a-zA-Z][a-zA-Z0-9\p{IsLatin-1Supplement}\-]{0,79})",
2734 "$1$2<a href=\"/$3\">$3</a>");
2736 var m = Regex.Match(retStr, "(^|[^a-zA-Z0-9_])[@@]([a-zA-Z0-9_]{1,20})");
2739 if (!AtList.Contains(m.Result("$2").ToLower())) AtList.Add(m.Result("$2").ToLower());
2743 retStr = Regex.Replace(retStr,
2744 "(^|[^a-zA-Z0-9_/])([@@])([a-zA-Z0-9_]{1,20})",
2745 "$1$2<a href=\"/$3\">$3</a>");
2748 var anchorRange = new List<range>();
2749 for (int i = 0; i < retStr.Length; i++)
2751 var index = retStr.IndexOf("<a ", i);
2752 if (index > -1 && index < retStr.Length)
2755 var toIndex = retStr.IndexOf("</a>", index);
2758 anchorRange.Add(new range(index, toIndex + 3));
2763 //retStr = Regex.Replace(retStr,
2764 // "(^|[^a-zA-Z0-9/&])([##])([0-9a-zA-Z_]*[a-zA-Z_]+[a-zA-Z0-9_\xc0-\xd6\xd8-\xf6\xf8-\xff]*)",
2765 // new MatchEvaluator(Function(mh As Match)
2766 // foreach (var rng in anchorRange)
2768 // if (mh.Index >= rng.fromIndex &&
2769 // mh.Index <= rng.toIndex) return mh.Result("$0");
2771 // if (IsNumeric(mh.Result("$3"))) return mh.Result("$0");
2774 // _hashList.Add("#" + mh.Result("$3"))
2776 // return mh.Result("$1") + "<a href=\"" + _protocol + "twitter.com/search?q=%23" + mh.Result("$3") + "\">" + mh.Result("$2$3") + "</a>";
2778 // RegexOptions.IgnoreCase)
2779 retStr = Regex.Replace(retStr,
2781 new MatchEvaluator(mh =>
2783 foreach (var rng in anchorRange)
2785 if (mh.Index >= rng.fromIndex &&
2786 mh.Index <= rng.toIndex) return mh.Result("$0");
2790 _hashList.Add("#" + mh.Result("$3"));
2792 return mh.Result("$1") + "<a href=\"https://twitter.com/search?q=%23" + mh.Result("$3") + "\">" + mh.Result("$2$3") + "</a>";
2794 RegexOptions.IgnoreCase);
2797 retStr = Regex.Replace(retStr, "(^|[^a-zA-Z0-9_/&##@@>=.~])(sm|nm)([0-9]{1,10})", "$1<a href=\"http://www.nicovideo.jp/watch/$2$3\">$2$3</a>");
2799 retStr = retStr.Replace("<<<<<tweenだいなり>>>>>", ">").Replace("<<<<<tweenしょうなり>>>>>", "<");
2801 //retStr = AdjustHtml(ShortUrl.Resolve(PreProcessUrl(retStr), true)) //IDN置換、短縮Uri解決、@リンクを相対→絶対にしてtarget属性付与
2802 retStr = AdjustHtml(PreProcessUrl(retStr)); //IDN置換、短縮Uri解決、@リンクを相対→絶対にしてtarget属性付与
2806 public async Task<string> CreateHtmlAnchorAsync(string text, List<string> AtList, TwitterEntities entities, List<MediaInfo> media)
2808 if (entities != null)
2810 if (entities.Urls != null)
2812 foreach (var ent in entities.Urls)
2814 ent.ExpandedUrl = await ShortUrl.Instance.ExpandUrlAsync(ent.ExpandedUrl)
2815 .ConfigureAwait(false);
2817 if (media != null && !media.Any(info => info.Url == ent.ExpandedUrl))
2818 media.Add(new MediaInfo(ent.ExpandedUrl));
2821 if (entities.Hashtags != null)
2825 this._hashList.AddRange(entities.Hashtags.Select(x => "#" + x.Text));
2828 if (entities.UserMentions != null)
2830 foreach (var ent in entities.UserMentions)
2832 var screenName = ent.ScreenName.ToLower();
2833 if (!AtList.Contains(screenName))
2834 AtList.Add(screenName);
2837 if (entities.Media != null)
2841 foreach (var ent in entities.Media)
2843 if (!media.Any(x => x.Url == ent.MediaUrl))
2845 if (ent.VideoInfo != null &&
2846 ent.Type == "animated_gif" || ent.Type == "video")
2848 //var videoUrl = ent.VideoInfo.Variants
2849 // .Where(v => v.ContentType == "video/mp4")
2850 // .OrderByDescending(v => v.Bitrate)
2851 // .Select(v => v.Url).FirstOrDefault();
2852 media.Add(new MediaInfo(ent.MediaUrl, ent.ExpandedUrl));
2855 media.Add(new MediaInfo(ent.MediaUrl));
2862 text = TweetFormatter.AutoLinkHtml(text, entities);
2864 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>");
2865 text = PreProcessUrl(text); //IDN置換
2871 public string CreateHtmlAnchor(string text, List<string> AtList, TwitterEntities entities, List<MediaInfo> media)
2873 return this.CreateHtmlAnchorAsync(text, AtList, entities, media).Result;
2877 /// Twitter APIから得たHTML形式のsource文字列を分析し、source名とURLに分離します
2879 public static Tuple<string, Uri> ParseSource(string sourceHtml)
2881 if (string.IsNullOrEmpty(sourceHtml))
2882 return Tuple.Create<string, Uri>("", null);
2887 // sourceHtmlの例: <a href="http://twitter.com" rel="nofollow">Twitter Web Client</a>
2889 var match = Regex.Match(sourceHtml, "^<a href=\"(?<uri>.+?)\".*?>(?<text>.+)</a>$", RegexOptions.IgnoreCase);
2892 sourceText = WebUtility.HtmlDecode(match.Groups["text"].Value);
2895 var uriStr = WebUtility.HtmlDecode(match.Groups["uri"].Value);
2896 sourceUri = new Uri(new Uri("https://twitter.com/"), uriStr);
2898 catch (UriFormatException)
2905 sourceText = WebUtility.HtmlDecode(sourceHtml);
2909 return Tuple.Create(sourceText, sourceUri);
2912 public TwitterApiStatus GetInfoApi()
2914 if (Twitter.AccountState != MyCommon.ACCOUNT_STATE.Valid) return null;
2916 if (MyCommon._endingFlag) return null;
2922 res = twCon.RateLimitStatus(ref content);
2926 this.ResetApiStatus();
2930 this.CheckStatusCode(res, content);
2934 MyCommon.TwitterApiInfo.UpdateFromJson(content);
2935 return MyCommon.TwitterApiInfo;
2937 catch (Exception ex)
2939 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
2940 MyCommon.TwitterApiInfo.Reset();
2946 /// ブロック中のユーザーを更新します
2948 /// <exception cref="WebApiException"/>
2949 public void RefreshBlockIds()
2951 if (MyCommon._endingFlag) return;
2954 var newBlockIds = new HashSet<long>();
2957 var ret = this.GetBlockIdsApi(cursor);
2958 newBlockIds.UnionWith(ret.Ids);
2959 cursor = ret.NextCursor;
2960 } while (cursor != 0);
2962 newBlockIds.Remove(this.UserId); // 元のソースにあったので一応残しておく
2964 TabInformations.GetInstance().BlockIds = newBlockIds;
2967 public TwitterIds GetBlockIdsApi(long cursor)
2969 this.CheckAccountState();
2975 res = twCon.GetBlockUserIds(ref content, cursor);
2979 throw new WebApiException("Err:" + e.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", e);
2982 this.CheckStatusCode(res, content);
2986 return TwitterIds.ParseJson(content);
2988 catch(SerializationException e)
2990 var ex = new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, e);
2991 MyCommon.TraceOut(ex);
2996 var ex = new WebApiException("Err:Invalid Json!", content, e);
2997 MyCommon.TraceOut(ex);
3003 /// ミュート中のユーザーIDを更新します
3005 /// <exception cref="WebApiException"/>
3006 public async Task RefreshMuteUserIdsAsync()
3008 if (MyCommon._endingFlag) return;
3010 var ids = await TwitterIds.GetAllItemsAsync(this.GetMuteUserIdsApiAsync)
3011 .ConfigureAwait(false);
3013 TabInformations.GetInstance().MuteUserIds = new HashSet<long>(ids);
3016 public async Task<TwitterIds> GetMuteUserIdsApiAsync(long cursor)
3022 var res = await Task.Run(() => twCon.GetMuteUserIds(ref content, cursor))
3023 .ConfigureAwait(false);
3025 this.CheckStatusCode(res, content);
3027 return TwitterIds.ParseJson(content);
3029 catch (WebException ex)
3031 var ex2 = new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", content, ex);
3032 MyCommon.TraceOut(ex2);
3035 catch (SerializationException ex)
3037 var ex2 = new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
3038 MyCommon.TraceOut(ex2);
3043 public string[] GetHashList()
3048 hashArray = _hashList.ToArray();
3054 public string AccessToken
3058 return twCon.AccessToken;
3062 public string AccessTokenSecret
3066 return twCon.AccessTokenSecret;
3070 private void CheckAccountState()
3072 if (Twitter.AccountState != MyCommon.ACCOUNT_STATE.Valid)
3073 throw new WebApiException("Auth error. Check your account");
3076 private void CheckAccessLevel(TwitterApiAccessLevel accessLevelFlags)
3078 if (!this.AccessLevel.HasFlag(accessLevelFlags))
3079 throw new WebApiException("Auth Err:try to re-authorization.");
3082 private void CheckStatusCode(HttpStatusCode httpStatus, string responseText,
3083 [CallerMemberName] string callerMethodName = "")
3085 if (httpStatus == HttpStatusCode.OK)
3087 Twitter.AccountState = MyCommon.ACCOUNT_STATE.Valid;
3091 if (string.IsNullOrWhiteSpace(responseText))
3093 if (httpStatus == HttpStatusCode.Unauthorized)
3094 Twitter.AccountState = MyCommon.ACCOUNT_STATE.Invalid;
3096 throw new WebApiException("Err:" + httpStatus + "(" + callerMethodName + ")");
3101 var errors = TwitterError.ParseJson(responseText).Errors;
3102 if (errors == null || !errors.Any())
3104 throw new WebApiException("Err:" + httpStatus + "(" + callerMethodName + ")", responseText);
3107 foreach (var error in errors)
3109 if (error.Code == TwitterErrorCode.InvalidToken ||
3110 error.Code == TwitterErrorCode.SuspendedAccount)
3112 Twitter.AccountState = MyCommon.ACCOUNT_STATE.Invalid;
3116 throw new WebApiException("Err:" + string.Join(",", errors.Select(x => x.ToString())) + "(" + callerMethodName + ")", responseText);
3118 catch (SerializationException) { }
3120 throw new WebApiException("Err:" + httpStatus + "(" + callerMethodName + ")", responseText);
3123 public int GetTextLengthRemain(string postText)
3125 var matchDm = Twitter.DMSendTextRegex.Match(postText);
3126 if (matchDm.Success)
3127 return this.GetTextLengthRemainInternal(matchDm.Groups["body"].Value, isDm: true);
3129 return this.GetTextLengthRemainInternal(postText, isDm: false);
3132 private int GetTextLengthRemainInternal(string postText, bool isDm)
3137 while (pos < postText.Length)
3141 if (char.IsSurrogatePair(postText, pos))
3142 pos += 2; // サロゲートペアの場合は2文字分進める
3147 var urlMatches = Regex.Matches(postText, Twitter.rgUrl, RegexOptions.IgnoreCase).Cast<Match>();
3148 foreach (var m in urlMatches)
3150 var before = m.Groups["before"].Value;
3151 var url = m.Groups["url"].Value;
3152 var protocol = m.Groups["protocol"].Value;
3153 var domain = m.Groups["domain"].Value;
3154 var path = m.Groups["path"].Value;
3155 if (protocol.Length == 0)
3157 if (Regex.IsMatch(before, Twitter.url_invalid_without_protocol_preceding_chars))
3160 var validUrl = false;
3161 string lasturl = null;
3163 var last_url_invalid_match = false;
3164 var domainMatches = Regex.Matches(domain, Twitter.url_valid_ascii_domain, RegexOptions.IgnoreCase).Cast<Match>();
3165 foreach (var mm in domainMatches)
3168 last_url_invalid_match = Regex.IsMatch(lasturl, Twitter.url_invalid_short_domain, RegexOptions.IgnoreCase);
3169 if (!last_url_invalid_match)
3175 if (last_url_invalid_match && path.Length != 0)
3182 textLength += this.Configuration.ShortUrlLength - url.Length;
3187 var shortUrlLength = protocol == "https://"
3188 ? this.Configuration.ShortUrlLengthHttps
3189 : this.Configuration.ShortUrlLength;
3191 textLength += shortUrlLength - url.Length;
3196 return this.Configuration.DmTextCharacterLimit - textLength;
3198 return 140 - textLength;
3201 #region "UserStream"
3202 private string trackWord_ = "";
3203 public string TrackWord
3214 private bool allAtReply_ = false;
3215 public bool AllAtReply
3223 allAtReply_ = value;
3227 public event EventHandler NewPostFromStream;
3228 public event EventHandler UserStreamStarted;
3229 public event EventHandler UserStreamStopped;
3230 public event EventHandler<PostDeletedEventArgs> PostDeleted;
3231 public event EventHandler<UserStreamEventReceivedEventArgs> UserStreamEventReceived;
3232 private DateTime _lastUserstreamDataReceived;
3233 private TwitterUserstream userStream;
3235 public class FormattedEvent
3237 public MyCommon.EVENTTYPE Eventtype { get; set; }
3238 public DateTime CreatedAt { get; set; }
3239 public string Event { get; set; }
3240 public string Username { get; set; }
3241 public string Target { get; set; }
3242 public Int64 Id { get; set; }
3243 public bool IsMe { get; set; }
3246 public List<FormattedEvent> storedEvent_ = new List<FormattedEvent>();
3247 public List<FormattedEvent> StoredEvent
3251 return storedEvent_;
3255 storedEvent_ = value;
3259 private readonly IReadOnlyDictionary<string, MyCommon.EVENTTYPE> eventTable = new Dictionary<string, MyCommon.EVENTTYPE>
3261 ["favorite"] = MyCommon.EVENTTYPE.Favorite,
3262 ["unfavorite"] = MyCommon.EVENTTYPE.Unfavorite,
3263 ["follow"] = MyCommon.EVENTTYPE.Follow,
3264 ["list_member_added"] = MyCommon.EVENTTYPE.ListMemberAdded,
3265 ["list_member_removed"] = MyCommon.EVENTTYPE.ListMemberRemoved,
3266 ["block"] = MyCommon.EVENTTYPE.Block,
3267 ["unblock"] = MyCommon.EVENTTYPE.Unblock,
3268 ["user_update"] = MyCommon.EVENTTYPE.UserUpdate,
3269 ["deleted"] = MyCommon.EVENTTYPE.Deleted,
3270 ["list_created"] = MyCommon.EVENTTYPE.ListCreated,
3271 ["list_destroyed"] = MyCommon.EVENTTYPE.ListDestroyed,
3272 ["list_updated"] = MyCommon.EVENTTYPE.ListUpdated,
3273 ["unfollow"] = MyCommon.EVENTTYPE.Unfollow,
3274 ["list_user_subscribed"] = MyCommon.EVENTTYPE.ListUserSubscribed,
3275 ["list_user_unsubscribed"] = MyCommon.EVENTTYPE.ListUserUnsubscribed,
3276 ["mute"] = MyCommon.EVENTTYPE.Mute,
3277 ["unmute"] = MyCommon.EVENTTYPE.Unmute,
3278 ["quoted_tweet"] = MyCommon.EVENTTYPE.QuotedTweet,
3281 public bool IsUserstreamDataReceived
3285 return DateTime.Now.Subtract(this._lastUserstreamDataReceived).TotalSeconds < 31;
3289 private void userStream_StatusArrived(string line)
3291 this._lastUserstreamDataReceived = DateTime.Now;
3292 if (string.IsNullOrEmpty(line)) return;
3294 if (line.First() != '{' || line.Last() != '}')
3296 MyCommon.TraceOut("Invalid JSON (StatusArrived):" + Environment.NewLine + line);
3304 using (var jsonReader = JsonReaderWriterFactory.CreateJsonReader(Encoding.UTF8.GetBytes(line), XmlDictionaryReaderQuotas.Max))
3306 var xElm = XElement.Load(jsonReader);
3307 if (xElm.Element("friends") != null)
3309 Debug.WriteLine("friends");
3312 else if (xElm.Element("delete") != null)
3314 Debug.WriteLine("delete");
3317 if ((idElm = xElm.Element("delete").Element("direct_message")?.Element("id")) != null)
3320 long.TryParse(idElm.Value, out id);
3322 this.PostDeleted?.Invoke(this, new PostDeletedEventArgs(id));
3324 else if ((idElm = xElm.Element("delete").Element("status")?.Element("id")) != null)
3327 long.TryParse(idElm.Value, out id);
3329 this.PostDeleted?.Invoke(this, new PostDeletedEventArgs(id));
3333 MyCommon.TraceOut("delete:" + line);
3336 for (int i = this.StoredEvent.Count - 1; i >= 0; i--)
3338 var sEvt = this.StoredEvent[i];
3339 if (sEvt.Id == id && (sEvt.Event == "favorite" || sEvt.Event == "unfavorite"))
3341 this.StoredEvent.RemoveAt(i);
3346 else if (xElm.Element("limit") != null)
3348 Debug.WriteLine(line);
3351 else if (xElm.Element("event") != null)
3353 Debug.WriteLine("event: " + xElm.Element("event").Value);
3354 CreateEventFromJson(line);
3357 else if (xElm.Element("direct_message") != null)
3359 Debug.WriteLine("direct_message");
3362 else if (xElm.Element("retweeted_status") != null)
3364 var sourceUserId = xElm.XPathSelectElement("/user/id_str").Value;
3365 var targetUserId = xElm.XPathSelectElement("/retweeted_status/user/id_str").Value;
3367 // 自分に関係しないリツイートの場合は無視する
3368 var selfUserId = this.UserId.ToString();
3369 if (sourceUserId == selfUserId || targetUserId == selfUserId)
3371 // 公式 RT をイベントとしても扱う
3372 var evt = CreateEventFromRetweet(xElm);
3375 this.StoredEvent.Insert(0, evt);
3377 this.UserStreamEventReceived?.Invoke(this, new UserStreamEventReceivedEventArgs(evt));
3381 // 従来通り公式 RT の表示も行うため return しない
3383 else if (xElm.Element("scrub_geo") != null)
3387 TabInformations.GetInstance().ScrubGeoReserve(long.Parse(xElm.Element("scrub_geo").Element("user_id").Value),
3388 long.Parse(xElm.Element("scrub_geo").Element("up_to_status_id").Value));
3392 MyCommon.TraceOut("scrub_geo:" + line);
3400 CreateDirectMessagesFromJson(line, MyCommon.WORKERTYPE.UserStream, false);
3404 CreatePostsFromJson("[" + line + "]", MyCommon.WORKERTYPE.Timeline, null, false);
3407 catch (WebApiException ex)
3409 MyCommon.TraceOut(ex);
3412 catch(NullReferenceException)
3414 MyCommon.TraceOut("NullRef StatusArrived: " + line);
3417 this.NewPostFromStream?.Invoke(this, EventArgs.Empty);
3421 /// UserStreamsから受信した公式RTをイベントに変換します
3423 private FormattedEvent CreateEventFromRetweet(XElement xElm)
3425 return new FormattedEvent
3427 Eventtype = MyCommon.EVENTTYPE.Retweet,
3429 CreatedAt = MyCommon.DateTimeParse(xElm.XPathSelectElement("/created_at").Value),
3430 IsMe = xElm.XPathSelectElement("/user/id_str").Value == this.UserId.ToString(),
3431 Username = xElm.XPathSelectElement("/user/screen_name").Value,
3432 Target = string.Format("@{0}:{1}", new[]
3434 xElm.XPathSelectElement("/retweeted_status/user/screen_name").Value,
3435 xElm.XPathSelectElement("/retweeted_status/text").Value,
3437 Id = long.Parse(xElm.XPathSelectElement("/retweeted_status/id_str").Value),
3441 private void CreateEventFromJson(string content)
3443 TwitterStreamEvent eventData = null;
3446 eventData = TwitterStreamEvent.ParseJson(content);
3448 catch(SerializationException ex)
3450 MyCommon.TraceOut(ex, "Event Serialize Exception!" + Environment.NewLine + content);
3454 MyCommon.TraceOut(ex, "Event Exception!" + Environment.NewLine + content);
3457 var evt = new FormattedEvent();
3458 evt.CreatedAt = MyCommon.DateTimeParse(eventData.CreatedAt);
3459 evt.Event = eventData.Event;
3460 evt.Username = eventData.Source.ScreenName;
3461 evt.IsMe = evt.Username.ToLower().Equals(this.Username.ToLower());
3463 MyCommon.EVENTTYPE eventType;
3464 eventTable.TryGetValue(eventData.Event, out eventType);
3465 evt.Eventtype = eventType;
3467 TwitterStreamEvent<TwitterStatus> tweetEvent;
3469 switch (eventData.Event)
3471 case "access_revoked":
3472 case "access_unrevoked":
3474 case "user_suspend":
3477 if (eventData.Target.ScreenName.ToLower().Equals(_uname))
3479 if (!this.followerId.Contains(eventData.Source.Id)) this.followerId.Add(eventData.Source.Id);
3483 return; //Block後のUndoをすると、SourceとTargetが逆転したfollowイベントが帰ってくるため。
3488 evt.Target = "@" + eventData.Target.ScreenName;
3490 case "favorited_retweet":
3491 case "retweeted_retweet":
3495 tweetEvent = TwitterStreamEvent<TwitterStatus>.ParseJson(content);
3496 evt.Target = "@" + tweetEvent.TargetObject.User.ScreenName + ":" + WebUtility.HtmlDecode(tweetEvent.TargetObject.Text);
3497 evt.Id = tweetEvent.TargetObject.Id;
3499 if (SettingCommon.Instance.IsRemoveSameEvent)
3501 if (this.StoredEvent.Any(ev => ev.Username == evt.Username && ev.Eventtype == evt.Eventtype && ev.Target == evt.Target))
3505 var tabinfo = TabInformations.GetInstance();
3508 var statusId = tweetEvent.TargetObject.Id;
3509 if (!tabinfo.Posts.TryGetValue(statusId, out post))
3512 if (eventData.Event == "favorite")
3514 var favTab = tabinfo.GetTabByType(MyCommon.TabUsageType.Favorites);
3515 if (!favTab.Contains(post.StatusId))
3516 favTab.Add(post.StatusId, post.IsRead, false);
3518 if (tweetEvent.Source.Id == this.UserId)
3522 else if (tweetEvent.Target.Id == this.UserId)
3524 post.FavoritedCount++;
3526 if (SettingCommon.Instance.FavEventUnread)
3527 tabinfo.SetReadAllTab(post.StatusId, read: false);
3532 if (tweetEvent.Source.Id == this.UserId)
3536 else if (tweetEvent.Target.Id == this.UserId)
3538 post.FavoritedCount = Math.Max(0, post.FavoritedCount - 1);
3542 case "quoted_tweet":
3543 if (evt.IsMe) return;
3545 tweetEvent = TwitterStreamEvent<TwitterStatus>.ParseJson(content);
3546 evt.Target = "@" + tweetEvent.TargetObject.User.ScreenName + ":" + WebUtility.HtmlDecode(tweetEvent.TargetObject.Text);
3547 evt.Id = tweetEvent.TargetObject.Id;
3549 if (SettingCommon.Instance.IsRemoveSameEvent)
3551 if (this.StoredEvent.Any(ev => ev.Username == evt.Username && ev.Eventtype == evt.Eventtype && ev.Target == evt.Target))
3555 case "list_member_added":
3556 case "list_member_removed":
3557 case "list_created":
3558 case "list_destroyed":
3559 case "list_updated":
3560 case "list_user_subscribed":
3561 case "list_user_unsubscribed":
3562 var listEvent = TwitterStreamEvent<TwitterList>.ParseJson(content);
3563 evt.Target = listEvent.TargetObject.FullName;
3566 if (!TabInformations.GetInstance().BlockIds.Contains(eventData.Target.Id)) TabInformations.GetInstance().BlockIds.Add(eventData.Target.Id);
3570 if (TabInformations.GetInstance().BlockIds.Contains(eventData.Target.Id)) TabInformations.GetInstance().BlockIds.Remove(eventData.Target.Id);
3579 evt.Target = "@" + eventData.Target.ScreenName;
3580 if (!TabInformations.GetInstance().MuteUserIds.Contains(eventData.Target.Id))
3582 TabInformations.GetInstance().MuteUserIds.Add(eventData.Target.Id);
3586 evt.Target = "@" + eventData.Target.ScreenName;
3587 if (TabInformations.GetInstance().MuteUserIds.Contains(eventData.Target.Id))
3589 TabInformations.GetInstance().MuteUserIds.Remove(eventData.Target.Id);
3594 MyCommon.TraceOut("Unknown Event:" + evt.Event + Environment.NewLine + content);
3597 this.StoredEvent.Insert(0, evt);
3599 this.UserStreamEventReceived?.Invoke(this, new UserStreamEventReceivedEventArgs(evt));
3602 private void userStream_Started()
3604 this.UserStreamStarted?.Invoke(this, EventArgs.Empty);
3607 private void userStream_Stopped()
3609 this.UserStreamStopped?.Invoke(this, EventArgs.Empty);
3612 public bool UserStreamEnabled
3616 return userStream == null ? false : userStream.Enabled;
3620 public void StartUserStream()
3622 if (userStream != null)
3626 userStream = new TwitterUserstream(twCon);
3627 userStream.StatusArrived += userStream_StatusArrived;
3628 userStream.Started += userStream_Started;
3629 userStream.Stopped += userStream_Stopped;
3630 userStream.Start(this.AllAtReply, this.TrackWord);
3633 public void StopUserStream()
3635 userStream?.Dispose();
3637 if (!MyCommon._endingFlag)
3639 this.UserStreamStopped?.Invoke(this, EventArgs.Empty);
3643 public void ReconnectUserStream()
3645 if (userStream != null)
3647 this.StartUserStream();
3651 private class TwitterUserstream : IDisposable
3653 public event Action<string> StatusArrived;
3654 public event Action Stopped;
3655 public event Action Started;
3656 private HttpTwitter twCon;
3658 private Thread _streamThread;
3659 private bool _streamActive;
3661 private bool _allAtreplies = false;
3662 private string _trackwords = "";
3664 public TwitterUserstream(HttpTwitter twitterConnection)
3666 twCon = (HttpTwitter)twitterConnection.Clone();
3669 public void Start(bool allAtReplies, string trackwords)
3671 this.AllAtReplies = allAtReplies;
3672 this.TrackWords = trackwords;
3673 _streamActive = true;
3674 if (_streamThread != null && _streamThread.IsAlive) return;
3675 _streamThread = new Thread(UserStreamLoop);
3676 _streamThread.Name = "UserStreamReceiver";
3677 _streamThread.IsBackground = true;
3678 _streamThread.Start();
3685 return _streamActive;
3689 public bool AllAtReplies
3693 return _allAtreplies;
3697 _allAtreplies = value;
3701 public string TrackWords
3709 _trackwords = value;
3713 private void UserStreamLoop()
3719 StreamReader sr = null;
3722 if (!MyCommon.IsNetworkAvailable())
3730 var res = twCon.UserStream(ref st, _allAtreplies, _trackwords, Networking.GetUserAgentString());
3734 case HttpStatusCode.OK:
3735 Twitter.AccountState = MyCommon.ACCOUNT_STATE.Valid;
3737 case HttpStatusCode.Unauthorized:
3738 Twitter.AccountState = MyCommon.ACCOUNT_STATE.Invalid;
3746 //MyCommon.TraceOut("Stop:stream is null")
3750 sr = new StreamReader(st);
3752 while (_streamActive && !sr.EndOfStream && Twitter.AccountState == MyCommon.ACCOUNT_STATE.Valid)
3754 StatusArrived?.Invoke(sr.ReadLine());
3755 //this.LastTime = Now;
3758 if (sr.EndOfStream || Twitter.AccountState == MyCommon.ACCOUNT_STATE.Invalid)
3761 //MyCommon.TraceOut("Stop:EndOfStream")
3766 catch(WebException ex)
3768 if (ex.Status == WebExceptionStatus.Timeout)
3770 sleepSec = 30; //MyCommon.TraceOut("Stop:Timeout")
3772 else if (ex.Response != null && (int)((HttpWebResponse)ex.Response).StatusCode == 420)
3774 //MyCommon.TraceOut("Stop:Connection Limit")
3780 //MyCommon.TraceOut("Stop:WebException " + ex.Status.ToString())
3783 catch(ThreadAbortException)
3790 //MyCommon.TraceOut("Stop:IOException with Active." + Environment.NewLine + ex.Message)
3792 catch(ArgumentException ex)
3794 //System.ArgumentException: ストリームを読み取れませんでした。
3795 //サーバー側もしくは通信経路上で切断された場合?タイムアウト頻発後発生
3797 MyCommon.TraceOut(ex, "Stop:ArgumentException");
3801 MyCommon.TraceOut("Stop:Exception." + Environment.NewLine + ex.Message);
3802 MyCommon.ExceptionOut(ex);
3811 twCon.RequestAbort();
3816 while (_streamActive && ms < sleepSec * 1000)
3824 } while (this._streamActive);
3830 MyCommon.TraceOut("Stop:EndLoop");
3833 #region "IDisposable Support"
3834 private bool disposedValue; // 重複する呼び出しを検出するには
3837 protected virtual void Dispose(bool disposing)
3839 if (!this.disposedValue)
3843 _streamActive = false;
3844 if (_streamThread != null && _streamThread.IsAlive)
3846 _streamThread.Abort();
3850 this.disposedValue = true;
3853 //protected Overrides void Finalize()
3855 // // このコードを変更しないでください。クリーンアップ コードを上の Dispose(bool disposing) に記述します。
3857 // MyBase.Finalize()
3860 // このコードは、破棄可能なパターンを正しく実装できるように Visual Basic によって追加されました。
3861 public void Dispose()
3863 // このコードを変更しないでください。クリーンアップ コードを上の Dispose(bool disposing) に記述します。
3865 GC.SuppressFinalize(this);
3872 #region "IDisposable Support"
3873 private bool disposedValue; // 重複する呼び出しを検出するには
3876 protected virtual void Dispose(bool disposing)
3878 if (!this.disposedValue)
3882 this.StopUserStream();
3885 this.disposedValue = true;
3888 //protected Overrides void Finalize()
3890 // // このコードを変更しないでください。クリーンアップ コードを上の Dispose(bool disposing) に記述します。
3892 // MyBase.Finalize()
3895 // このコードは、破棄可能なパターンを正しく実装できるように Visual Basic によって追加されました。
3896 public void Dispose()
3898 // このコードを変更しないでください。クリーンアップ コードを上の Dispose(bool disposing) に記述します。
3900 GC.SuppressFinalize(this);
3905 public class PostDeletedEventArgs : EventArgs
3907 public long StatusId { get; }
3909 public PostDeletedEventArgs(long statusId)
3911 this.StatusId = statusId;
3915 public class UserStreamEventReceivedEventArgs : EventArgs
3917 public Twitter.FormattedEvent EventData { get; }
3919 public UserStreamEventReceivedEventArgs(Twitter.FormattedEvent eventData)
3921 this.EventData = eventData;