1 // OpenTween - Client of Twitter
2 // Copyright (c) 2007-2011 kiri_feather (@kiri_feather) <kiri.feather@gmail.com>
3 // (c) 2008-2011 Moz (@syo68k)
4 // (c) 2008-2011 takeshik (@takeshik) <http://www.takeshik.org/>
5 // (c) 2010-2011 anis774 (@anis774) <http://d.hatena.ne.jp/anis774/>
6 // (c) 2010-2011 fantasticswallow (@f_swallow) <http://twitter.com/f_swallow>
7 // (c) 2011 Egtra (@egtra) <http://dev.activebasic.com/egtra/>
8 // (c) 2013 kim_upsilon (@kim_upsilon) <https://upsilo.net/~upsilon/>
9 // All rights reserved.
11 // This file is part of OpenTween.
13 // This program is free software; you can redistribute it and/or modify it
14 // under the terms of the GNU General Public License as published by the Free
15 // Software Foundation; either version 3 of the License, or (at your option)
18 // This program is distributed in the hope that it will be useful, but
19 // WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
20 // or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
23 // You should have received a copy of the GNU General Public License along
24 // with this program. If not, see <http://www.gnu.org/licenses/>, or write to
25 // the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor,
26 // Boston, MA 02110-1301, USA.
28 using System.Diagnostics;
32 using System.Runtime.CompilerServices;
33 using System.Runtime.Serialization;
34 using System.Runtime.Serialization.Json;
36 using System.Text.RegularExpressions;
37 using System.Threading;
38 using System.Threading.Tasks;
41 using System.Xml.Linq;
42 using System.Xml.XPath;
44 using System.Reflection;
45 using System.Collections.Generic;
47 using System.Windows.Forms;
49 using OpenTween.Api.DataModel;
50 using OpenTween.Connection;
54 public class Twitter : IDisposable
56 #region Regexp from twitter-text-js
58 // The code in this region code block incorporates works covered by
59 // the following copyright and permission notices:
61 // Copyright 2011 Twitter, Inc.
63 // Licensed under the Apache License, Version 2.0 (the "License"); you
64 // may not use this work except in compliance with the License. You
65 // may obtain a copy of the License in the LICENSE file, or at:
67 // http://www.apache.org/licenses/LICENSE-2.0
69 // Unless required by applicable law or agreed to in writing, software
70 // distributed under the License is distributed on an "AS IS" BASIS,
71 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
72 // implied. See the License for the specific language governing
73 // permissions and limitations under the License.
76 private const string LATIN_ACCENTS = @"\u00c0-\u00d6\u00d8-\u00f6\u00f8-\u00ff\u0100-\u024f\u0253\u0254\u0256\u0257\u0259\u025b\u0263\u0268\u026f\u0272\u0289\u028b\u02bb\u1e00-\u1eff";
77 private const string NON_LATIN_HASHTAG_CHARS = @"\u0400-\u04ff\u0500-\u0527\u1100-\u11ff\u3130-\u3185\uA960-\uA97F\uAC00-\uD7AF\uD7B0-\uD7FF";
78 //private const string CJ_HASHTAG_CHARACTERS = @"\u30A1-\u30FA\uFF66-\uFF9F\uFF10-\uFF19\uFF21-\uFF3A\uFF41-\uFF5A\u3041-\u3096\u3400-\u4DBF\u4E00-\u9FFF\u20000-\u2A6DF\u2A700-\u2B73F\u2B740-\u2B81F\u2F800-\u2FA1F";
79 private const string CJ_HASHTAG_CHARACTERS = @"\u30A1-\u30FA\u30FC\u3005\uFF66-\uFF9F\uFF10-\uFF19\uFF21-\uFF3A\uFF41-\uFF5A\u3041-\u309A\u3400-\u4DBF\p{IsCJKUnifiedIdeographs}";
80 private const string HASHTAG_BOUNDARY = @"^|$|\s|「|」|。|\.|!";
81 private const string HASHTAG_ALPHA = "[a-z_" + LATIN_ACCENTS + NON_LATIN_HASHTAG_CHARS + CJ_HASHTAG_CHARACTERS + "]";
82 private const string HASHTAG_ALPHANUMERIC = "[a-z0-9_" + LATIN_ACCENTS + NON_LATIN_HASHTAG_CHARS + CJ_HASHTAG_CHARACTERS + "]";
83 private const string HASHTAG_TERMINATOR = "[^a-z0-9_" + LATIN_ACCENTS + NON_LATIN_HASHTAG_CHARS + CJ_HASHTAG_CHARACTERS + "]";
84 public const string HASHTAG = "(" + HASHTAG_BOUNDARY + ")(#|#)(" + HASHTAG_ALPHANUMERIC + "*" + HASHTAG_ALPHA + HASHTAG_ALPHANUMERIC + "*)(?=" + HASHTAG_TERMINATOR + "|" + HASHTAG_BOUNDARY + ")";
86 private const string url_valid_preceding_chars = @"(?:[^A-Za-z0-9@@$##\ufffe\ufeff\uffff\u202a-\u202e]|^)";
87 public const string url_invalid_without_protocol_preceding_chars = @"[-_./]$";
88 private const string url_invalid_domain_chars = @"\!'#%&'\(\)*\+,\\\-\.\/:;<=>\?@\[\]\^_{|}~\$\u2000-\u200a\u0009-\u000d\u0020\u0085\u00a0\u1680\u180e\u2028\u2029\u202f\u205f\u3000\ufffe\ufeff\uffff\u202a-\u202e";
89 private const string url_valid_domain_chars = @"[^" + url_invalid_domain_chars + "]";
90 private const string url_valid_subdomain = @"(?:(?:" + url_valid_domain_chars + @"(?:[_-]|" + url_valid_domain_chars + @")*)?" + url_valid_domain_chars + @"\.)";
91 private const string url_valid_domain_name = @"(?:(?:" + url_valid_domain_chars + @"(?:-|" + url_valid_domain_chars + @")*)?" + url_valid_domain_chars + @"\.)";
92 private const string url_valid_GTLD = @"(?:(?:aero|asia|biz|cat|com|coop|edu|gov|info|int|jobs|mil|mobi|museum|name|net|org|pro|tel|travel|xxx)(?=[^0-9a-zA-Z]|$))";
93 private const string url_valid_CCTLD = @"(?:(?:ac|ad|ae|af|ag|ai|al|am|an|ao|aq|ar|as|at|au|aw|ax|az|ba|bb|bd|be|bf|bg|bh|bi|bj|bm|bn|bo|br|bs|bt|bv|bw|by|bz|ca|cc|cd|cf|cg|ch|ci|ck|cl|cm|cn|co|cr|cs|cu|cv|cx|cy|cz|dd|de|dj|dk|dm|do|dz|ec|ee|eg|eh|er|es|et|eu|fi|fj|fk|fm|fo|fr|ga|gb|gd|ge|gf|gg|gh|gi|gl|gm|gn|gp|gq|gr|gs|gt|gu|gw|gy|hk|hm|hn|hr|ht|hu|id|ie|il|im|in|io|iq|ir|is|it|je|jm|jo|jp|ke|kg|kh|ki|km|kn|kp|kr|kw|ky|kz|la|lb|lc|li|lk|lr|ls|lt|lu|lv|ly|ma|mc|md|me|mg|mh|mk|ml|mm|mn|mo|mp|mq|mr|ms|mt|mu|mv|mw|mx|my|mz|na|nc|ne|nf|ng|ni|nl|no|np|nr|nu|nz|om|pa|pe|pf|pg|ph|pk|pl|pm|pn|pr|ps|pt|pw|py|qa|re|ro|rs|ru|rw|sa|sb|sc|sd|se|sg|sh|si|sj|sk|sl|sm|sn|so|sr|ss|st|su|sv|sy|sz|tc|td|tf|tg|th|tj|tk|tl|tm|tn|to|tp|tr|tt|tv|tw|tz|ua|ug|uk|us|uy|uz|va|vc|ve|vg|vi|vn|vu|wf|ws|ye|yt|za|zm|zw)(?=[^0-9a-zA-Z]|$))";
94 private const string url_valid_punycode = @"(?:xn--[0-9a-z]+)";
95 private const string url_valid_domain = @"(?<domain>" + url_valid_subdomain + "*" + url_valid_domain_name + "(?:" + url_valid_GTLD + "|" + url_valid_CCTLD + ")|" + url_valid_punycode + ")";
96 public const string url_valid_ascii_domain = @"(?:(?:[a-z0-9" + LATIN_ACCENTS + @"]+)\.)+(?:" + url_valid_GTLD + "|" + url_valid_CCTLD + "|" + url_valid_punycode + ")";
97 public const string url_invalid_short_domain = "^" + url_valid_domain_name + url_valid_CCTLD + "$";
98 private const string url_valid_port_number = @"[0-9]+";
100 private const string url_valid_general_path_chars = @"[a-z0-9!*';:=+,.$/%#\[\]\-_~|&" + LATIN_ACCENTS + "]";
101 private const string url_balance_parens = @"(?:\(" + url_valid_general_path_chars + @"+\))";
102 private const string url_valid_path_ending_chars = @"(?:[+\-a-z0-9=_#/" + LATIN_ACCENTS + "]|" + url_balance_parens + ")";
103 private const string pth = "(?:" +
105 url_valid_general_path_chars + "*" +
106 "(?:" + url_balance_parens + url_valid_general_path_chars + "*)*" +
107 url_valid_path_ending_chars +
108 ")|(?:@" + url_valid_general_path_chars + "+/)" +
110 private const string qry = @"(?<query>\?[a-z0-9!?*'();:&=+$/%#\[\]\-_.,~|]*[a-z0-9_&=#/])?";
111 public const string rgUrl = @"(?<before>" + url_valid_preceding_chars + ")" +
112 "(?<url>(?<protocol>https?://)?" +
113 "(?<domain>" + url_valid_domain + ")" +
114 "(?::" + url_valid_port_number + ")?" +
115 "(?<path>/" + pth + "*)?" +
122 /// Twitter API のステータスページのURL
124 public const string ServiceAvailabilityStatusUrl = "https://status.io.watchmouse.com/7617";
127 /// ツイートへのパーマリンクURLを判定する正規表現
129 public static readonly Regex StatusUrlRegex = new Regex(@"https?://([^.]+\.)?twitter\.com/(#!/)?(?<ScreenName>[a-zA-Z0-9_]+)/status(es)?/(?<StatusId>[0-9]+)(/photo)?", RegexOptions.IgnoreCase);
132 /// FavstarやaclogなどTwitter関連サービスのパーマリンクURLからステータスIDを抽出する正規表現
134 public static readonly Regex ThirdPartyStatusUrlRegex = new Regex(@"https?://(?:[^.]+\.)?(?:
135 favstar\.fm/users/[a-zA-Z0-9_]+/status/ # Favstar
136 | favstar\.fm/t/ # Favstar (short)
137 | aclog\.koba789\.com/i/ # aclog
138 | frtrt\.net/solo_status\.php\?status= # RtRT
139 )(?<StatusId>[0-9]+)", RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace);
142 /// DM送信かどうかを判定する正規表現
144 public static readonly Regex DMSendTextRegex = new Regex(@"^DM? +(?<id>[a-zA-Z0-9_]+) +(?<body>.*)", RegexOptions.IgnoreCase | RegexOptions.Singleline);
146 public TwitterConfiguration Configuration { get; private set; }
148 delegate void GetIconImageDelegate(PostClass post);
149 private readonly object LockObj = new object();
150 private ISet<long> followerId = new HashSet<long>();
151 private bool _GetFollowerResult = false;
152 private long[] noRTId = new long[0];
153 private bool _GetNoRetweetResult = false;
156 private string _uname;
158 private bool _readOwnPost;
159 private List<string> _hashList = new List<string>();
161 //max_idで古い発言を取得するために保持(lists分は個別タブで管理)
162 private long minHomeTimeline = long.MaxValue;
163 private long minMentions = long.MaxValue;
164 private long minDirectmessage = long.MaxValue;
165 private long minDirectmessageSent = long.MaxValue;
167 //private FavoriteQueue favQueue;
169 private HttpTwitter twCon = new HttpTwitter();
171 //private List<PostClass> _deletemessages = new List<PostClass>();
175 this.Configuration = TwitterConfiguration.DefaultConfiguration();
178 public TwitterApiAccessLevel AccessLevel
182 return MyCommon.TwitterApiInfo.AccessLevel;
186 protected void ResetApiStatus()
188 MyCommon.TwitterApiInfo.Reset();
191 public void Authenticate(string username, string password)
193 this.ResetApiStatus();
199 res = twCon.AuthUserAndPass(username, password, ref content);
203 throw new WebApiException("Err:" + ex.Message, ex);
206 this.CheckStatusCode(res, content);
208 _uname = username.ToLowerInvariant();
209 if (SettingCommon.Instance.UserstreamStartup) this.ReconnectUserStream();
212 public string StartAuthentication()
215 this.ResetApiStatus();
218 string pinPageUrl = null;
219 var res = twCon.AuthGetRequestToken(ref pinPageUrl);
221 throw new WebApiException("Err:Failed to access auth server.");
227 throw new WebApiException("Err:Failed to access auth server.", ex);
231 public void Authenticate(string pinCode)
233 this.ResetApiStatus();
238 res = twCon.AuthGetAccessToken(pinCode);
242 throw new WebApiException("Err:Failed to access auth acc server.", ex);
245 this.CheckStatusCode(res, null);
247 _uname = Username.ToLowerInvariant();
248 if (SettingCommon.Instance.UserstreamStartup) this.ReconnectUserStream();
251 public void ClearAuthInfo()
253 Twitter.AccountState = MyCommon.ACCOUNT_STATE.Invalid;
254 this.ResetApiStatus();
255 twCon.ClearAuthInfo();
258 public void VerifyCredentials()
264 res = twCon.VerifyCredentials(ref content);
268 throw new WebApiException("Err:" + ex.Message, ex);
271 this.CheckStatusCode(res, content);
275 var user = TwitterUser.ParseJson(content);
277 this.twCon.AuthenticatedUserId = user.Id;
278 this.UpdateUserStats(user);
280 catch (SerializationException ex)
282 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
283 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
287 public void Initialize(string token, string tokenSecret, string username, long userId)
290 if (string.IsNullOrEmpty(token) || string.IsNullOrEmpty(tokenSecret) || string.IsNullOrEmpty(username))
292 Twitter.AccountState = MyCommon.ACCOUNT_STATE.Invalid;
294 this.ResetApiStatus();
295 twCon.Initialize(token, tokenSecret, username, userId);
296 _uname = username.ToLowerInvariant();
297 if (SettingCommon.Instance.UserstreamStartup) this.ReconnectUserStream();
300 public string PreProcessUrl(string orgData)
304 //var IDNConveter = new IdnMapping();
305 var href = "<a href=\"";
309 if (orgData.IndexOf(href, posl2, StringComparison.Ordinal) > -1)
313 posl1 = orgData.IndexOf(href, posl2, StringComparison.Ordinal);
314 posl1 += href.Length;
315 posl2 = orgData.IndexOf("\"", posl1, StringComparison.Ordinal);
316 urlStr = orgData.Substring(posl1, posl2 - posl1);
318 if (!urlStr.StartsWith("http://", StringComparison.Ordinal)
319 && !urlStr.StartsWith("https://", StringComparison.Ordinal)
320 && !urlStr.StartsWith("ftp://", StringComparison.Ordinal))
325 var replacedUrl = MyCommon.IDNEncode(urlStr);
326 if (replacedUrl == null) continue;
327 if (replacedUrl == urlStr) continue;
329 orgData = orgData.Replace("<a href=\"" + urlStr, "<a href=\"" + replacedUrl);
340 private string GetPlainText(string orgData)
342 return WebUtility.HtmlDecode(Regex.Replace(orgData, "(?<tagStart><a [^>]+>)(?<text>[^<]+)(?<tagEnd></a>)", "${text}"));
345 // htmlの簡易サニタイズ(詳細表示に不要なタグの除去)
347 private string SanitizeHtml(string orgdata)
349 var retdata = orgdata;
351 retdata = Regex.Replace(retdata, "<(script|object|applet|image|frameset|fieldset|legend|style).*" +
352 "</(script|object|applet|image|frameset|fieldset|legend|style)>", "", RegexOptions.IgnoreCase);
354 retdata = Regex.Replace(retdata, "<(frame|link|iframe|img)>", "", RegexOptions.IgnoreCase);
359 private string AdjustHtml(string orgData)
361 var retStr = orgData;
362 //var m = Regex.Match(retStr, "<a [^>]+>[#|#](?<1>[a-zA-Z0-9_]+)</a>");
367 // _hashList.Add("#" + m.Groups(1).Value);
371 retStr = Regex.Replace(retStr, "<a [^>]*href=\"/", "<a href=\"https://twitter.com/");
372 retStr = retStr.Replace("<a href=", "<a target=\"_self\" href=");
373 retStr = Regex.Replace(retStr, @"(\r\n?|\n)", "<br>"); // CRLF, CR, LF は全て <br> に置換する
375 //半角スペースを置換(Thanks @anis774)
379 ret = EscapeSpace(ref retStr);
382 return SanitizeHtml(retStr);
385 private bool EscapeSpace(ref string html)
387 //半角スペースを置換(Thanks @anis774)
389 for (int i = 0; i < html.Length; i++)
400 if ((!isTag) && (html[i] == ' '))
402 html = html.Remove(i, 1);
403 html = html.Insert(i, " ");
410 private struct PostInfo
412 public string CreatedAt;
415 public string UserId;
416 public PostInfo(string Created, string IdStr, string txt, string uid)
423 public bool Equals(PostInfo dst)
425 if (this.CreatedAt == dst.CreatedAt && this.Id == dst.Id && this.Text == dst.Text && this.UserId == dst.UserId)
436 static private PostInfo _prev = new PostInfo("", "", "", "");
437 private bool IsPostRestricted(TwitterStatus status)
439 var _current = new PostInfo("", "", "", "");
441 _current.CreatedAt = status.CreatedAt;
442 _current.Id = status.IdStr;
443 if (status.Text == null)
449 _current.Text = status.Text;
451 _current.UserId = status.User.IdStr;
453 if (_current.Equals(_prev))
457 _prev.CreatedAt = _current.CreatedAt;
458 _prev.Id = _current.Id;
459 _prev.Text = _current.Text;
460 _prev.UserId = _current.UserId;
465 public void PostStatus(string postStr, long? reply_to, List<long> mediaIds = null)
467 this.CheckAccountState();
469 if (mediaIds == null &&
470 Twitter.DMSendTextRegex.IsMatch(postStr))
472 SendDirectMessage(postStr);
480 res = twCon.UpdateStatus(postStr, reply_to, mediaIds, ref content);
484 throw new WebApiException("Err:" + ex.Message, ex);
487 // 投稿に成功していても404が返ることがあるらしい: https://dev.twitter.com/discussions/1213
488 if (res == HttpStatusCode.NotFound)
491 this.CheckStatusCode(res, content);
493 TwitterStatus status;
496 status = TwitterStatus.ParseJson(content);
498 catch(SerializationException ex)
500 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
501 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
505 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
506 throw new WebApiException("Err:Invalid Json!", content, ex);
509 this.UpdateUserStats(status.User);
511 if (IsPostRestricted(status))
513 throw new WebApiException("OK:Delaying?");
517 public void PostStatusWithMedia(string postStr, long? reply_to, IMediaItem item)
519 this.CheckAccountState();
525 res = twCon.UpdateStatusWithMedia(postStr, reply_to, item, ref content);
529 throw new WebApiException("Err:" + ex.Message, ex);
532 // 投稿に成功していても404が返ることがあるらしい: https://dev.twitter.com/discussions/1213
533 if (res == HttpStatusCode.NotFound)
536 this.CheckStatusCode(res, content);
538 TwitterStatus status;
541 status = TwitterStatus.ParseJson(content);
543 catch(SerializationException ex)
545 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
546 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
550 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
551 throw new WebApiException("Err:Invalid Json!", content, ex);
554 this.UpdateUserStats(status.User);
556 if (IsPostRestricted(status))
558 throw new WebApiException("OK:Delaying?");
562 public void PostStatusWithMultipleMedia(string postStr, long? reply_to, IMediaItem[] mediaItems)
564 this.CheckAccountState();
566 if (Twitter.DMSendTextRegex.IsMatch(postStr))
568 SendDirectMessage(postStr);
572 var mediaIds = new List<long>();
574 foreach (var item in mediaItems)
576 var mediaId = UploadMedia(item);
577 mediaIds.Add(mediaId);
580 if (mediaIds.Count == 0)
581 throw new WebApiException("Err:Invalid Files!");
583 PostStatus(postStr, reply_to, mediaIds);
586 public long UploadMedia(IMediaItem item)
588 this.CheckAccountState();
594 res = twCon.UploadMedia(item, ref content);
598 throw new WebApiException("Err:" + ex.Message, ex);
601 this.CheckStatusCode(res, content);
603 TwitterUploadMediaResult status;
606 status = TwitterUploadMediaResult.ParseJson(content);
608 catch (SerializationException ex)
610 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
611 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
615 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
616 throw new WebApiException("Err:Invalid Json!", content, ex);
619 return status.MediaId;
622 public void SendDirectMessage(string postStr)
624 this.CheckAccountState();
625 this.CheckAccessLevel(TwitterApiAccessLevel.ReadWriteAndDirectMessage);
627 var mc = Twitter.DMSendTextRegex.Match(postStr);
633 res = twCon.SendDirectMessage(mc.Groups["body"].Value, mc.Groups["id"].Value, ref content);
637 throw new WebApiException("Err:" + ex.Message, ex);
640 this.CheckStatusCode(res, content);
642 TwitterDirectMessage status;
645 status = TwitterDirectMessage.ParseJson(content);
647 catch(SerializationException ex)
649 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
650 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
654 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
655 throw new WebApiException("Err:Invalid Json!", content, ex);
658 this.UpdateUserStats(status.Sender);
661 public void PostRetweet(long id, bool read)
663 this.CheckAccountState();
667 var post = TabInformations.GetInstance()[id];
670 throw new WebApiException("Err:Target isn't found.");
672 if (TabInformations.GetInstance()[id].RetweetedId != null)
674 target = TabInformations.GetInstance()[id].RetweetedId.Value; //再RTの場合は元発言をRT
681 res = twCon.RetweetStatus(target, ref content);
685 throw new WebApiException("Err:" + ex.Message, ex);
688 this.CheckStatusCode(res, content);
690 TwitterStatus status;
693 status = TwitterStatus.ParseJson(content);
695 catch(SerializationException ex)
697 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
698 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
702 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
703 throw new WebApiException("Err:Invalid Json!", content, ex);
707 post = CreatePostsFromStatusData(status);
709 throw new WebApiException("Invalid Json!", content);
714 if (TabInformations.GetInstance().ContainsKey(post.StatusId))
718 if (post.RetweetedId == null)
719 throw new WebApiException("Invalid Json!", content);
725 if (_readOwnPost) post.IsRead = true;
728 TabInformations.GetInstance().AddPost(post);
731 public void PostCreateBlock(string screenName)
733 this.CheckAccountState();
739 res = twCon.CreateBlock(screenName, ref content);
743 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
746 this.CheckStatusCode(res, content);
749 public void PostDestroyBlock(string screenName)
751 this.CheckAccountState();
757 res = twCon.DestroyBlock(screenName, ref content);
761 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
764 this.CheckStatusCode(res, content);
767 public void PostReportSpam(string screenName)
769 this.CheckAccountState();
775 res = twCon.ReportSpam(screenName, ref content);
779 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
782 this.CheckStatusCode(res, content);
785 public TwitterUser GetUserInfo(string screenName)
787 this.CheckAccountState();
793 res = twCon.ShowUserInfo(screenName, ref content);
797 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
800 this.CheckStatusCode(res, content);
804 return TwitterUser.ParseJson(content);
806 catch (SerializationException ex)
808 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
809 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
813 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
814 throw new WebApiException("Err:Invalid Json!", content, ex);
818 public int GetStatus_Retweeted_Count(long StatusId)
820 this.CheckAccountState();
826 res = twCon.ShowStatuses(StatusId, ref content);
830 throw new WebApiException("Err:" + ex.Message, ex);
833 this.CheckStatusCode(res, content);
837 var status = TwitterStatus.ParseJson(content);
838 return status.RetweetCount;
840 catch (SerializationException ex)
842 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
843 throw new WebApiException("Json Parse Error(DataContractJsonSerializer)", content, ex);
847 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
848 throw new WebApiException("Invalid Json!", content, ex);
852 public void PostFavAdd(long id)
854 this.CheckAccountState();
856 //if (this.favQueue == null) this.favQueue = new FavoriteQueue(this)
858 //if (this.favQueue.Contains(id)) this.favQueue.Remove(id)
864 res = twCon.CreateFavorites(id, ref content);
868 //this.favQueue.Add(id)
869 //return "Err:->FavoriteQueue:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")";
870 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
873 this.CheckStatusCode(res, content);
875 if (!RestrictFavCheck)
878 //http://twitter.com/statuses/show/id.xml APIを発行して本文を取得
882 res = twCon.ShowStatuses(id, ref content);
886 throw new WebApiException("Err:" + ex.Message, ex);
889 this.CheckStatusCode(res, content);
891 TwitterStatus status;
894 status = TwitterStatus.ParseJson(content);
896 catch (SerializationException ex)
898 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
899 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
903 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
904 throw new WebApiException("Err:Invalid Json!", content, ex);
906 if (status.Favorited != true)
907 throw new WebApiException("NG(Restricted?)");
910 public void PostFavRemove(long id)
912 this.CheckAccountState();
914 //if (this.favQueue == null) this.favQueue = new FavoriteQueue(this)
916 //if (this.favQueue.Contains(id))
917 // this.favQueue.Remove(id)
925 res = twCon.DestroyFavorites(id, ref content);
929 throw new WebApiException("Err:" + ex.Message, ex);
932 this.CheckStatusCode(res, content);
935 public TwitterUser PostUpdateProfile(string name, string url, string location, string description)
937 this.CheckAccountState();
943 res = twCon.UpdateProfile(name, url, location, description, ref content);
947 throw new WebApiException("Err:" + ex.Message, content, ex);
950 this.CheckStatusCode(res, content);
954 return TwitterUser.ParseJson(content);
956 catch (SerializationException e)
958 var ex = new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, e);
959 MyCommon.TraceOut(ex);
964 var ex = new WebApiException("Err:Invalid Json!", content, e);
965 MyCommon.TraceOut(ex);
970 public void PostUpdateProfileImage(string filename)
972 this.CheckAccountState();
978 res = twCon.UpdateProfileImage(new FileInfo(filename), ref content);
982 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
985 this.CheckStatusCode(res, content);
988 public string Username
992 return twCon.AuthenticatedUsername;
1000 return twCon.AuthenticatedUserId;
1004 public string Password
1008 return twCon.Password;
1012 private static MyCommon.ACCOUNT_STATE _accountState = MyCommon.ACCOUNT_STATE.Valid;
1013 public static MyCommon.ACCOUNT_STATE AccountState
1017 return _accountState;
1021 _accountState = value;
1025 public bool RestrictFavCheck { get; set; }
1028 public void GetTweenBinary(string strVer)
1033 if (!(new HttpVarious()).GetDataToFile("http://tween.sourceforge.jp/Tween" + strVer + ".gz?" + DateTime.Now.ToString("yyMMddHHmmss") + Environment.TickCount.ToString(),
1034 Path.Combine(MyCommon.settingPath, "TweenNew.exe")))
1036 throw new WebApiException("Err:Download failed");
1039 if (!Directory.Exists(Path.Combine(MyCommon.settingPath, "en")))
1041 Directory.CreateDirectory(Path.Combine(MyCommon.settingPath, "en"));
1043 if (!(new HttpVarious()).GetDataToFile("http://tween.sourceforge.jp/TweenResEn" + strVer + ".gz?" + DateTime.Now.ToString("yyMMddHHmmss") + Environment.TickCount.ToString(),
1044 Path.Combine(Path.Combine(MyCommon.settingPath, "en"), "Tween.resourcesNew.dll")))
1046 throw new WebApiException("Err:Download failed");
1048 //その他言語圏のリソース。取得失敗しても継続
1051 if (!Thread.CurrentThread.CurrentUICulture.IsNeutralCulture)
1053 var idx = Thread.CurrentThread.CurrentUICulture.Name.LastIndexOf('-');
1056 curCul = Thread.CurrentThread.CurrentUICulture.Name.Substring(0, idx);
1060 curCul = Thread.CurrentThread.CurrentUICulture.Name;
1065 curCul = Thread.CurrentThread.CurrentUICulture.Name;
1067 if (!string.IsNullOrEmpty(curCul) && curCul != "en" && curCul != "ja")
1069 if (!Directory.Exists(Path.Combine(MyCommon.settingPath, curCul)))
1071 Directory.CreateDirectory(Path.Combine(MyCommon.settingPath, curCul));
1073 if (!(new HttpVarious()).GetDataToFile("http://tween.sourceforge.jp/TweenRes" + curCul + strVer + ".gz?" + DateTime.Now.ToString("yyMMddHHmmss") + Environment.TickCount.ToString(),
1074 Path.Combine(Path.Combine(MyCommon.settingPath, curCul), "Tween.resourcesNew.dll")))
1076 //return "Err:Download failed";
1081 if (!Thread.CurrentThread.CurrentCulture.IsNeutralCulture)
1083 var idx = Thread.CurrentThread.CurrentCulture.Name.LastIndexOf('-');
1086 curCul2 = Thread.CurrentThread.CurrentCulture.Name.Substring(0, idx);
1090 curCul2 = Thread.CurrentThread.CurrentCulture.Name;
1095 curCul2 = Thread.CurrentThread.CurrentCulture.Name;
1097 if (!string.IsNullOrEmpty(curCul2) && curCul2 != "en" && curCul2 != curCul)
1099 if (!Directory.Exists(Path.Combine(MyCommon.settingPath, curCul2)))
1101 Directory.CreateDirectory(Path.Combine(MyCommon.settingPath, curCul2));
1103 if (!(new HttpVarious()).GetDataToFile("http://tween.sourceforge.jp/TweenRes" + curCul2 + strVer + ".gz?" + DateTime.Now.ToString("yyMMddHHmmss") + Environment.TickCount.ToString(),
1104 Path.Combine(Path.Combine(MyCommon.settingPath, curCul2), "Tween.resourcesNew.dll")))
1106 //return "Err:Download failed";
1111 if (!(new HttpVarious()).GetDataToFile("http://tween.sourceforge.jp/TweenUp3.gz?" + DateTime.Now.ToString("yyMMddHHmmss") + Environment.TickCount.ToString(),
1112 Path.Combine(MyCommon.settingPath, "TweenUp3.exe")))
1114 throw new WebApiException("Err:Download failed");
1117 if (!(new HttpVarious()).GetDataToFile("http://tween.sourceforge.jp/TweenDll" + strVer + ".gz?" + DateTime.Now.ToString("yyMMddHHmmss") + Environment.TickCount.ToString(),
1118 Path.Combine(MyCommon.settingPath, "TweenNew.XmlSerializers.dll")))
1120 throw new WebApiException("Err:Download failed");
1123 catch (Exception ex)
1125 throw new WebApiException("Err:Download failed", ex);
1130 public bool ReadOwnPost
1134 return _readOwnPost;
1138 _readOwnPost = value;
1142 public int FollowersCount { get; private set; }
1143 public int FriendsCount { get; private set; }
1144 public int StatusesCount { get; private set; }
1145 public string Location { get; private set; } = "";
1146 public string Bio { get; private set; } = "";
1148 /// <summary>ユーザーのフォロワー数などの情報を更新します</summary>
1149 private void UpdateUserStats(TwitterUser self)
1151 this.FollowersCount = self.FollowersCount;
1152 this.FriendsCount = self.FriendsCount;
1153 this.StatusesCount = self.StatusesCount;
1154 this.Location = self.Location;
1155 this.Bio = self.Description;
1159 /// 渡された取得件数がWORKERTYPEに応じた取得可能範囲に収まっているか検証する
1161 public static bool VerifyApiResultCount(MyCommon.WORKERTYPE type, int count)
1163 return count >= 20 && count <= GetMaxApiResultCount(type);
1167 /// 渡された取得件数が更新時の取得可能範囲に収まっているか検証する
1169 public static bool VerifyMoreApiResultCount(int count)
1171 return count >= 20 && count <= 200;
1175 /// 渡された取得件数が起動時の取得可能範囲に収まっているか検証する
1177 public static bool VerifyFirstApiResultCount(int count)
1179 return count >= 20 && count <= 200;
1183 /// WORKERTYPEに応じた取得可能な最大件数を取得する
1185 public static int GetMaxApiResultCount(MyCommon.WORKERTYPE type)
1187 // 参照: REST APIs - 各endpointのcountパラメータ
1188 // https://dev.twitter.com/rest/public
1191 case MyCommon.WORKERTYPE.Timeline:
1192 case MyCommon.WORKERTYPE.Reply:
1193 case MyCommon.WORKERTYPE.UserTimeline:
1194 case MyCommon.WORKERTYPE.Favorites:
1195 case MyCommon.WORKERTYPE.DirectMessegeRcv:
1196 case MyCommon.WORKERTYPE.DirectMessegeSnt:
1197 case MyCommon.WORKERTYPE.List: // 不明
1200 case MyCommon.WORKERTYPE.PublicSearch:
1204 throw new InvalidOperationException("Invalid type: " + type);
1209 /// WORKERTYPEに応じた取得件数を取得する
1211 public static int GetApiResultCount(MyCommon.WORKERTYPE type, bool more, bool startup)
1213 if (type == MyCommon.WORKERTYPE.DirectMessegeRcv ||
1214 type == MyCommon.WORKERTYPE.DirectMessegeSnt)
1219 if (SettingCommon.Instance.UseAdditionalCount)
1223 case MyCommon.WORKERTYPE.Favorites:
1224 if (SettingCommon.Instance.FavoritesCountApi != 0)
1225 return SettingCommon.Instance.FavoritesCountApi;
1227 case MyCommon.WORKERTYPE.List:
1228 if (SettingCommon.Instance.ListCountApi != 0)
1229 return SettingCommon.Instance.ListCountApi;
1231 case MyCommon.WORKERTYPE.PublicSearch:
1232 if (SettingCommon.Instance.SearchCountApi != 0)
1233 return SettingCommon.Instance.SearchCountApi;
1235 case MyCommon.WORKERTYPE.UserTimeline:
1236 if (SettingCommon.Instance.UserTimelineCountApi != 0)
1237 return SettingCommon.Instance.UserTimelineCountApi;
1240 if (more && SettingCommon.Instance.MoreCountApi != 0)
1242 return Math.Min(SettingCommon.Instance.MoreCountApi, GetMaxApiResultCount(type));
1244 if (startup && SettingCommon.Instance.FirstCountApi != 0 && type != MyCommon.WORKERTYPE.Reply)
1246 return Math.Min(SettingCommon.Instance.FirstCountApi, GetMaxApiResultCount(type));
1250 // 上記に当てはまらない場合の共通処理
1251 var count = SettingCommon.Instance.CountApi;
1253 if (type == MyCommon.WORKERTYPE.Reply)
1254 count = SettingCommon.Instance.CountApiReply;
1256 return Math.Min(count, GetMaxApiResultCount(type));
1259 public void GetTimelineApi(bool read,
1260 MyCommon.WORKERTYPE gType,
1264 this.CheckAccountState();
1268 var count = GetApiResultCount(gType, more, startup);
1272 if (gType == MyCommon.WORKERTYPE.Timeline)
1276 res = twCon.HomeTimeline(count, this.minHomeTimeline, null, ref content);
1280 res = twCon.HomeTimeline(count, null, null, ref content);
1287 res = twCon.Mentions(count, this.minMentions, null, ref content);
1291 res = twCon.Mentions(count, null, null, ref content);
1297 throw new WebApiException("Err:" + ex.Message, ex);
1300 this.CheckStatusCode(res, content);
1302 var minimumId = CreatePostsFromJson(content, gType, null, read);
1304 if (minimumId != null)
1306 if (gType == MyCommon.WORKERTYPE.Timeline)
1307 this.minHomeTimeline = minimumId.Value;
1309 this.minMentions = minimumId.Value;
1313 public void GetUserTimelineApi(bool read,
1318 this.CheckAccountState();
1322 var count = GetApiResultCount(MyCommon.WORKERTYPE.UserTimeline, more, false);
1326 if (string.IsNullOrEmpty(userName))
1328 var target = tab.User;
1329 if (string.IsNullOrEmpty(target)) return;
1331 res = twCon.UserTimeline(null, target, count, null, null, ref content);
1337 res = twCon.UserTimeline(null, userName, count, tab.OldestId, null, ref content);
1341 res = twCon.UserTimeline(null, userName, count, null, null, ref content);
1347 throw new WebApiException("Err:" + ex.Message, ex);
1350 if (res == HttpStatusCode.Unauthorized)
1351 throw new WebApiException("Err:@" + userName + "'s Tweets are protected.");
1353 this.CheckStatusCode(res, content);
1355 var minimumId = CreatePostsFromJson(content, MyCommon.WORKERTYPE.UserTimeline, tab, read);
1357 if (minimumId != null)
1358 tab.OldestId = minimumId.Value;
1361 public PostClass GetStatusApi(bool read, long id)
1363 this.CheckAccountState();
1369 res = twCon.ShowStatuses(id, ref content);
1373 throw new WebApiException("Err:" + ex.Message, ex);
1376 if (res == HttpStatusCode.Forbidden)
1377 throw new WebApiException("Err:protected user's tweet", content);
1379 this.CheckStatusCode(res, content);
1381 TwitterStatus status;
1384 status = TwitterStatus.ParseJson(content);
1386 catch(SerializationException ex)
1388 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
1389 throw new WebApiException("Json Parse Error(DataContractJsonSerializer)", content, ex);
1393 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
1394 throw new WebApiException("Invalid Json!", content, ex);
1397 var item = CreatePostsFromStatusData(status);
1399 throw new WebApiException("Err:Can't create post", content);
1402 if (item.IsMe && !read && _readOwnPost) item.IsRead = true;
1407 public void GetStatusApi(bool read, long id, TabClass tab)
1409 var post = this.GetStatusApi(read, id);
1411 //非同期アイコン取得&StatusDictionaryに追加
1412 if (tab != null && tab.IsInnerStorageTabType)
1413 tab.AddPostToInnerStorage(post);
1415 TabInformations.GetInstance().AddPost(post);
1418 private PostClass CreatePostsFromStatusData(TwitterStatus status)
1420 return CreatePostsFromStatusData(status, false);
1423 private PostClass CreatePostsFromStatusData(TwitterStatus status, bool favTweet)
1425 var post = new PostClass();
1426 TwitterEntities entities;
1429 post.StatusId = status.Id;
1430 if (status.RetweetedStatus != null)
1432 var retweeted = status.RetweetedStatus;
1434 post.CreatedAt = MyCommon.DateTimeParse(retweeted.CreatedAt);
1437 post.RetweetedId = retweeted.Id;
1439 post.TextFromApi = retweeted.Text;
1440 entities = retweeted.MergedEntities;
1441 sourceHtml = retweeted.Source;
1443 post.InReplyToStatusId = retweeted.InReplyToStatusId;
1444 post.InReplyToUser = retweeted.InReplyToScreenName;
1445 post.InReplyToUserId = status.InReplyToUserId;
1454 var tc = TabInformations.GetInstance().GetTabByType(MyCommon.TabUsageType.Favorites);
1455 post.IsFav = tc.Contains(retweeted.Id);
1458 if (retweeted.Coordinates != null)
1459 post.PostGeo = new PostClass.StatusGeo(retweeted.Coordinates.Coordinates[0], retweeted.Coordinates.Coordinates[1]);
1462 var user = retweeted.User;
1464 if (user == null || user.ScreenName == null || status.User.ScreenName == null) return null;
1466 post.UserId = user.Id;
1467 post.ScreenName = user.ScreenName;
1468 post.Nickname = user.Name.Trim();
1469 post.ImageUrl = user.ProfileImageUrlHttps;
1470 post.IsProtect = user.Protected;
1473 post.RetweetedBy = status.User.ScreenName;
1474 post.RetweetedByUserId = status.User.Id;
1475 post.IsMe = post.RetweetedBy.ToLowerInvariant().Equals(_uname);
1479 post.CreatedAt = MyCommon.DateTimeParse(status.CreatedAt);
1481 post.TextFromApi = status.Text;
1482 entities = status.MergedEntities;
1483 sourceHtml = status.Source;
1484 post.InReplyToStatusId = status.InReplyToStatusId;
1485 post.InReplyToUser = status.InReplyToScreenName;
1486 post.InReplyToUserId = status.InReplyToUserId;
1495 var tc = TabInformations.GetInstance().GetTabByType(MyCommon.TabUsageType.Favorites);
1496 post.IsFav = tc.Contains(post.StatusId) && TabInformations.GetInstance()[post.StatusId].IsFav;
1499 if (status.Coordinates != null)
1500 post.PostGeo = new PostClass.StatusGeo(status.Coordinates.Coordinates[0], status.Coordinates.Coordinates[1]);
1503 var user = status.User;
1505 if (user == null || user.ScreenName == null) return null;
1507 post.UserId = user.Id;
1508 post.ScreenName = user.ScreenName;
1509 post.Nickname = user.Name.Trim();
1510 post.ImageUrl = user.ProfileImageUrlHttps;
1511 post.IsProtect = user.Protected;
1512 post.IsMe = post.ScreenName.ToLowerInvariant().Equals(_uname);
1515 string textFromApi = post.TextFromApi;
1516 post.Text = CreateHtmlAnchor(textFromApi, post.ReplyToList, entities, post.Media);
1517 post.TextFromApi = textFromApi;
1518 post.TextFromApi = this.ReplaceTextFromApi(post.TextFromApi, entities);
1519 post.TextFromApi = WebUtility.HtmlDecode(post.TextFromApi);
1520 post.TextFromApi = post.TextFromApi.Replace("<3", "\u2661");
1522 post.QuoteStatusIds = GetQuoteTweetStatusIds(entities)
1523 .Where(x => x != post.StatusId && x != post.RetweetedId)
1524 .Distinct().ToArray();
1526 post.ExpandedUrls = entities.OfType<TwitterEntityUrl>()
1527 .Select(x => new PostClass.ExpandedUrlInfo(x.Url, x.ExpandedUrl))
1531 var source = ParseSource(sourceHtml);
1532 post.Source = source.Item1;
1533 post.SourceUri = source.Item2;
1535 post.IsReply = post.ReplyToList.Contains(_uname);
1536 post.IsExcludeReply = false;
1544 if (followerId.Count > 0) post.IsOwl = !followerId.Contains(post.UserId);
1552 /// ツイートに含まれる引用ツイートのURLからステータスIDを抽出
1554 public static IEnumerable<long> GetQuoteTweetStatusIds(IEnumerable<TwitterEntity> entities)
1556 var urls = entities.OfType<TwitterEntityUrl>().Select(x => x.ExpandedUrl);
1558 return GetQuoteTweetStatusIds(urls);
1561 public static IEnumerable<long> GetQuoteTweetStatusIds(IEnumerable<string> urls)
1563 foreach (var url in urls)
1565 var match = Twitter.StatusUrlRegex.Match(url);
1569 if (long.TryParse(match.Groups["StatusId"].Value, out statusId))
1570 yield return statusId;
1575 private long? CreatePostsFromJson(string content, MyCommon.WORKERTYPE gType, TabClass tab, bool read)
1577 TwitterStatus[] items;
1580 items = TwitterStatus.ParseJsonArray(content);
1582 catch(SerializationException ex)
1584 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
1585 throw new WebApiException("Json Parse Error(DataContractJsonSerializer)", content, ex);
1589 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
1590 throw new WebApiException("Invalid Json!", content, ex);
1593 long? minimumId = null;
1595 foreach (var status in items)
1597 PostClass post = null;
1598 post = CreatePostsFromStatusData(status);
1599 if (post == null) continue;
1601 if (minimumId == null || minimumId.Value > post.StatusId)
1602 minimumId = post.StatusId;
1609 if (TabInformations.GetInstance().ContainsKey(post.StatusId)) continue;
1613 if (tab.Contains(post.StatusId)) continue;
1618 if (gType != MyCommon.WORKERTYPE.UserTimeline &&
1619 post.RetweetedByUserId != null && this.noRTId.Contains(post.RetweetedByUserId.Value)) continue;
1622 if (post.IsMe && !read && _readOwnPost) post.IsRead = true;
1624 //非同期アイコン取得&StatusDictionaryに追加
1625 if (tab != null && tab.IsInnerStorageTabType)
1626 tab.AddPostToInnerStorage(post);
1628 TabInformations.GetInstance().AddPost(post);
1634 private long? CreatePostsFromSearchJson(string content, TabClass tab, bool read, int count, bool more)
1636 TwitterSearchResult items;
1639 items = TwitterSearchResult.ParseJson(content);
1641 catch (SerializationException ex)
1643 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
1644 throw new WebApiException("Json Parse Error(DataContractJsonSerializer)", content, ex);
1646 catch (Exception ex)
1648 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
1649 throw new WebApiException("Invalid Json!", content, ex);
1652 long? minimumId = null;
1654 foreach (var result in items.Statuses)
1656 PostClass post = null;
1657 post = CreatePostsFromStatusData(result);
1661 // Search API は相変わらずぶっ壊れたデータを返すことがあるため、必要なデータが欠如しているものは取得し直す
1664 post = this.GetStatusApi(read, result.Id);
1666 catch (WebApiException)
1672 if (minimumId == null || minimumId.Value > post.StatusId)
1673 minimumId = post.StatusId;
1675 if (!more && post.StatusId > tab.SinceId) tab.SinceId = post.StatusId;
1681 if (TabInformations.GetInstance().ContainsKey(post.StatusId)) continue;
1685 if (tab.Contains(post.StatusId)) continue;
1690 if ((post.IsMe && !read) && this._readOwnPost) post.IsRead = true;
1692 //非同期アイコン取得&StatusDictionaryに追加
1693 if (tab != null && tab.IsInnerStorageTabType)
1694 tab.AddPostToInnerStorage(post);
1696 TabInformations.GetInstance().AddPost(post);
1702 private void CreateFavoritePostsFromJson(string content, bool read)
1704 TwitterStatus[] item;
1707 item = TwitterStatus.ParseJsonArray(content);
1709 catch (SerializationException ex)
1711 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
1712 throw new WebApiException("Json Parse Error(DataContractJsonSerializer)", content, ex);
1714 catch (Exception ex)
1716 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
1717 throw new WebApiException("Invalid Json!", content, ex);
1720 var favTab = TabInformations.GetInstance().GetTabByType(MyCommon.TabUsageType.Favorites);
1722 foreach (var status in item)
1727 if (favTab.Contains(status.Id)) continue;
1730 var post = CreatePostsFromStatusData(status, true);
1731 if (post == null) continue;
1735 TabInformations.GetInstance().AddPost(post);
1739 public void GetListStatus(bool read,
1746 var count = GetApiResultCount(MyCommon.WORKERTYPE.List, more, startup);
1752 res = twCon.GetListsStatuses(tab.ListInfo.UserId, tab.ListInfo.Id, count, tab.OldestId, null, SettingCommon.Instance.IsListsIncludeRts, ref content);
1756 res = twCon.GetListsStatuses(tab.ListInfo.UserId, tab.ListInfo.Id, count, null, null, SettingCommon.Instance.IsListsIncludeRts, ref content);
1761 throw new WebApiException("Err:" + ex.Message, ex);
1764 this.CheckStatusCode(res, content);
1766 var minimumId = CreatePostsFromJson(content, MyCommon.WORKERTYPE.List, tab, read);
1768 if (minimumId != null)
1769 tab.OldestId = minimumId.Value;
1773 /// startStatusId からリプライ先の発言を辿る。発言は posts 以外からは検索しない。
1775 /// <returns>posts の中から検索されたリプライチェインの末端</returns>
1776 internal static PostClass FindTopOfReplyChain(IDictionary<Int64, PostClass> posts, Int64 startStatusId)
1778 if (!posts.ContainsKey(startStatusId))
1779 throw new ArgumentException("startStatusId (" + startStatusId + ") が posts の中から見つかりませんでした。", nameof(startStatusId));
1781 var nextPost = posts[startStatusId];
1782 while (nextPost.InReplyToStatusId != null)
1784 if (!posts.ContainsKey(nextPost.InReplyToStatusId.Value))
1786 nextPost = posts[nextPost.InReplyToStatusId.Value];
1792 public void GetRelatedResult(bool read, TabClass tab)
1794 var relPosts = new Dictionary<Int64, PostClass>();
1795 if (tab.RelationTargetPost.TextFromApi.Contains("@") && tab.RelationTargetPost.InReplyToStatusId == null)
1798 var p = TabInformations.GetInstance()[tab.RelationTargetPost.StatusId];
1799 if (p != null && p.InReplyToStatusId != null)
1801 tab.RelationTargetPost = p;
1805 p = this.GetStatusApi(read, tab.RelationTargetPost.StatusId);
1806 tab.RelationTargetPost = p;
1809 relPosts.Add(tab.RelationTargetPost.StatusId, tab.RelationTargetPost);
1811 Exception lastException = null;
1813 // in_reply_to_status_id を使用してリプライチェインを辿る
1814 var nextPost = FindTopOfReplyChain(relPosts, tab.RelationTargetPost.StatusId);
1816 while (nextPost.InReplyToStatusId != null && loopCount++ <= 20)
1818 var inReplyToId = nextPost.InReplyToStatusId.Value;
1820 var inReplyToPost = TabInformations.GetInstance()[inReplyToId];
1821 if (inReplyToPost == null)
1825 inReplyToPost = this.GetStatusApi(read, inReplyToId);
1827 catch (WebApiException ex)
1834 relPosts.Add(inReplyToPost.StatusId, inReplyToPost);
1836 nextPost = FindTopOfReplyChain(relPosts, nextPost.StatusId);
1839 //MRTとかに対応のためツイート内にあるツイートを指すURLを取り込む
1840 var text = tab.RelationTargetPost.Text;
1841 var ma = Twitter.StatusUrlRegex.Matches(text).Cast<Match>()
1842 .Concat(Twitter.ThirdPartyStatusUrlRegex.Matches(text).Cast<Match>());
1843 foreach (var _match in ma)
1846 if (Int64.TryParse(_match.Groups["StatusId"].Value, out _statusId))
1848 if (relPosts.ContainsKey(_statusId))
1851 var p = TabInformations.GetInstance()[_statusId];
1856 p = this.GetStatusApi(read, _statusId);
1858 catch (WebApiException ex)
1866 relPosts.Add(p.StatusId, p);
1870 relPosts.Values.ToList().ForEach(p =>
1872 if (p.IsMe && !read && this._readOwnPost)
1877 tab.AddPostToInnerStorage(p);
1880 if (lastException != null)
1881 throw new WebApiException(lastException.Message, lastException);
1884 public void GetSearch(bool read,
1890 var count = GetApiResultCount(MyCommon.WORKERTYPE.PublicSearch, more, false);
1892 long? sinceId = null;
1895 maxId = tab.OldestId - 1;
1899 sinceId = tab.SinceId;
1904 // TODO:一時的に40>100件に 件数変更UI作成の必要あり
1905 res = twCon.Search(tab.SearchWords, tab.SearchLang, count, maxId, sinceId, ref content);
1909 throw new WebApiException("Err:" + ex.Message, ex);
1913 case HttpStatusCode.BadRequest:
1914 throw new WebApiException("Invalid query", content);
1915 case HttpStatusCode.NotFound:
1916 throw new WebApiException("Invalid query", content);
1917 case HttpStatusCode.PaymentRequired: //API Documentには420と書いてあるが、該当コードがないので402にしてある
1918 throw new WebApiException("Search API Limit?", content);
1919 case HttpStatusCode.OK:
1922 throw new WebApiException("Err:" + res.ToString() + "(" + MethodBase.GetCurrentMethod().Name + ")", content);
1925 if (!TabInformations.GetInstance().ContainsTab(tab))
1928 var minimumId = this.CreatePostsFromSearchJson(content, tab, read, count, more);
1930 if (minimumId != null)
1931 tab.OldestId = minimumId.Value;
1934 private void CreateDirectMessagesFromJson(string content, MyCommon.WORKERTYPE gType, bool read)
1936 TwitterDirectMessage[] item;
1939 if (gType == MyCommon.WORKERTYPE.UserStream)
1941 item = new[] { TwitterStreamEventDirectMessage.ParseJson(content).DirectMessage };
1945 item = TwitterDirectMessage.ParseJsonArray(content);
1948 catch(SerializationException ex)
1950 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
1951 throw new WebApiException("Json Parse Error(DataContractJsonSerializer)", content, ex);
1955 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
1956 throw new WebApiException("Invalid Json!", content, ex);
1959 foreach (var message in item)
1961 var post = new PostClass();
1964 post.StatusId = message.Id;
1965 if (gType != MyCommon.WORKERTYPE.UserStream)
1967 if (gType == MyCommon.WORKERTYPE.DirectMessegeRcv)
1969 if (minDirectmessage > post.StatusId) minDirectmessage = post.StatusId;
1973 if (minDirectmessageSent > post.StatusId) minDirectmessageSent = post.StatusId;
1980 if (TabInformations.GetInstance().GetTabByType(MyCommon.TabUsageType.DirectMessage).Contains(post.StatusId)) continue;
1984 post.CreatedAt = MyCommon.DateTimeParse(message.CreatedAt);
1986 var textFromApi = message.Text;
1988 post.Text = CreateHtmlAnchor(textFromApi, post.ReplyToList, message.Entities, post.Media);
1989 post.TextFromApi = this.ReplaceTextFromApi(textFromApi, message.Entities);
1990 post.TextFromApi = WebUtility.HtmlDecode(post.TextFromApi);
1991 post.TextFromApi = post.TextFromApi.Replace("<3", "\u2661");
1994 post.QuoteStatusIds = GetQuoteTweetStatusIds(message.Entities).Distinct().ToArray();
1996 post.ExpandedUrls = message.Entities.OfType<TwitterEntityUrl>()
1997 .Select(x => new PostClass.ExpandedUrlInfo(x.Url, x.ExpandedUrl))
2002 if (gType == MyCommon.WORKERTYPE.UserStream)
2004 if (twCon.AuthenticatedUsername.Equals(message.Recipient.ScreenName, StringComparison.CurrentCultureIgnoreCase))
2006 user = message.Sender;
2012 user = message.Recipient;
2019 if (gType == MyCommon.WORKERTYPE.DirectMessegeRcv)
2021 user = message.Sender;
2027 user = message.Recipient;
2033 post.UserId = user.Id;
2034 post.ScreenName = user.ScreenName;
2035 post.Nickname = user.Name.Trim();
2036 post.ImageUrl = user.ProfileImageUrlHttps;
2037 post.IsProtect = user.Protected;
2041 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
2042 MessageBox.Show("Parse Error(CreateDirectMessagesFromJson)");
2047 if (post.IsMe && !read && _readOwnPost) post.IsRead = true;
2048 post.IsReply = false;
2049 post.IsExcludeReply = false;
2052 var dmTab = TabInformations.GetInstance().GetTabByType(MyCommon.TabUsageType.DirectMessage);
2053 dmTab.AddPostToInnerStorage(post);
2057 public void GetDirectMessageApi(bool read,
2058 MyCommon.WORKERTYPE gType,
2061 this.CheckAccountState();
2062 this.CheckAccessLevel(TwitterApiAccessLevel.ReadWriteAndDirectMessage);
2066 var count = GetApiResultCount(gType, more, false);
2070 if (gType == MyCommon.WORKERTYPE.DirectMessegeRcv)
2074 res = twCon.DirectMessages(count, minDirectmessage, null, ref content);
2078 res = twCon.DirectMessages(count, null, null, ref content);
2085 res = twCon.DirectMessagesSent(count, minDirectmessageSent, null, ref content);
2089 res = twCon.DirectMessagesSent(count, null, null, ref content);
2095 throw new WebApiException("Err:" + ex.Message, ex);
2098 this.CheckStatusCode(res, content);
2100 CreateDirectMessagesFromJson(content, gType, read);
2103 public void GetFavoritesApi(bool read,
2106 this.CheckAccountState();
2110 var count = GetApiResultCount(MyCommon.WORKERTYPE.Favorites, more, false);
2114 res = twCon.Favorites(count, ref content);
2118 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
2121 this.CheckStatusCode(res, content);
2123 CreateFavoritePostsFromJson(content, read);
2126 private string ReplaceTextFromApi(string text, TwitterEntities entities)
2128 if (entities != null)
2130 if (entities.Urls != null)
2132 foreach (var m in entities.Urls)
2134 if (!string.IsNullOrEmpty(m.DisplayUrl)) text = text.Replace(m.Url, m.DisplayUrl);
2137 if (entities.Media != null)
2139 foreach (var m in entities.Media)
2141 if (m.AltText != null)
2143 text = text.Replace(m.Url, string.Format(Properties.Resources.ImageAltText, m.AltText));
2147 if (!string.IsNullOrEmpty(m.DisplayUrl)) text = text.Replace(m.Url, m.DisplayUrl);
2158 /// <exception cref="WebApiException"/>
2159 public void RefreshFollowerIds()
2161 if (MyCommon._endingFlag) return;
2164 var newFollowerIds = new HashSet<long>();
2167 var ret = this.GetFollowerIdsApi(ref cursor);
2168 newFollowerIds.UnionWith(ret.Ids);
2169 cursor = ret.NextCursor;
2170 } while (cursor != 0);
2172 this.followerId = newFollowerIds;
2173 TabInformations.GetInstance().RefreshOwl(this.followerId);
2175 this._GetFollowerResult = true;
2178 public bool GetFollowersSuccess
2182 return _GetFollowerResult;
2186 private TwitterIds GetFollowerIdsApi(ref long cursor)
2188 this.CheckAccountState();
2194 res = twCon.FollowerIds(cursor, ref content);
2198 throw new WebApiException("Err:" + e.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", e);
2201 this.CheckStatusCode(res, content);
2205 var ret = TwitterIds.ParseJson(content);
2207 if (ret.Ids == null)
2209 var ex = new WebApiException("Err: ret.id == null (GetFollowerIdsApi)", content);
2210 MyCommon.ExceptionOut(ex);
2216 catch(SerializationException e)
2218 var ex = new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, e);
2219 MyCommon.TraceOut(ex);
2224 var ex = new WebApiException("Err:Invalid Json!", content, e);
2225 MyCommon.TraceOut(ex);
2231 /// RT 非表示ユーザーを更新します
2233 /// <exception cref="WebApiException"/>
2234 public void RefreshNoRetweetIds()
2236 if (MyCommon._endingFlag) return;
2238 this.noRTId = this.NoRetweetIdsApi();
2240 this._GetNoRetweetResult = true;
2243 private long[] NoRetweetIdsApi()
2245 this.CheckAccountState();
2251 res = twCon.NoRetweetIds(ref content);
2255 throw new WebApiException("Err:" + e.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", e);
2258 this.CheckStatusCode(res, content);
2262 return MyCommon.CreateDataFromJson<long[]>(content);
2264 catch(SerializationException e)
2266 var ex = new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, e);
2267 MyCommon.TraceOut(ex);
2272 var ex = new WebApiException("Err:Invalid Json!", content, e);
2273 MyCommon.TraceOut(ex);
2278 public bool GetNoRetweetSuccess
2282 return _GetNoRetweetResult;
2287 /// t.co の文字列長などの設定情報を更新します
2289 /// <exception cref="WebApiException"/>
2290 public void RefreshConfiguration()
2292 this.Configuration = this.ConfigurationApi();
2295 private TwitterConfiguration ConfigurationApi()
2301 res = twCon.GetConfiguration(ref content);
2305 throw new WebApiException("Err:" + e.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", e);
2308 this.CheckStatusCode(res, content);
2312 return TwitterConfiguration.ParseJson(content);
2314 catch(SerializationException e)
2316 var ex = new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, e);
2317 MyCommon.TraceOut(ex);
2322 var ex = new WebApiException("Err:Invalid Json!", content, e);
2323 MyCommon.TraceOut(ex);
2328 public void GetListsApi()
2330 this.CheckAccountState();
2333 IEnumerable<ListElement> lists;
2338 res = twCon.GetLists(this.Username, ref content);
2340 catch (Exception ex)
2342 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
2345 this.CheckStatusCode(res, content);
2349 lists = TwitterList.ParseJsonArray(content)
2350 .Select(x => new ListElement(x, this));
2352 catch (SerializationException ex)
2354 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
2355 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
2357 catch (Exception ex)
2359 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
2360 throw new WebApiException("Err:Invalid Json!", content, ex);
2365 res = twCon.GetListsSubscriptions(this.Username, ref content);
2367 catch (Exception ex)
2369 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
2372 this.CheckStatusCode(res, content);
2376 lists = lists.Concat(TwitterList.ParseJsonArray(content)
2377 .Select(x => new ListElement(x, this)));
2379 catch (SerializationException ex)
2381 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
2382 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
2384 catch (Exception ex)
2386 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
2387 throw new WebApiException("Err:Invalid Json!", content, ex);
2390 TabInformations.GetInstance().SubscribableLists = lists.ToList();
2393 public void DeleteList(string list_id)
2400 res = twCon.DeleteListID(this.Username, list_id, ref content);
2404 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
2407 this.CheckStatusCode(res, content);
2410 public ListElement EditList(string list_id, string new_name, bool isPrivate, string description)
2417 res = twCon.UpdateListID(this.Username, list_id, new_name, isPrivate, description, ref content);
2421 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
2424 this.CheckStatusCode(res, content);
2428 var le = TwitterList.ParseJson(content);
2429 return new ListElement(le, this);
2431 catch(SerializationException ex)
2433 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
2434 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
2438 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
2439 throw new WebApiException("Err:Invalid Json!", content, ex);
2443 public long GetListMembers(string list_id, List<UserInfo> lists, long cursor)
2445 this.CheckAccountState();
2451 res = twCon.GetListMembers(this.Username, list_id, cursor, ref content);
2455 throw new WebApiException("Err:" + ex.Message);
2458 this.CheckStatusCode(res, content);
2462 var users = TwitterUsers.ParseJson(content);
2463 Array.ForEach<TwitterUser>(
2465 u => lists.Add(new UserInfo(u)));
2467 return users.NextCursor;
2469 catch(SerializationException ex)
2471 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
2472 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
2476 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
2477 throw new WebApiException("Err:Invalid Json!", content, ex);
2481 public void CreateListApi(string listName, bool isPrivate, string description)
2483 this.CheckAccountState();
2489 res = twCon.CreateLists(listName, isPrivate, description, ref content);
2493 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
2496 this.CheckStatusCode(res, content);
2500 var le = TwitterList.ParseJson(content);
2501 TabInformations.GetInstance().SubscribableLists.Add(new ListElement(le, this));
2503 catch(SerializationException ex)
2505 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
2506 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
2510 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
2511 throw new WebApiException("Err:Invalid Json!", content, ex);
2515 public bool ContainsUserAtList(string listId, string user)
2517 this.CheckAccountState();
2524 res = this.twCon.ShowListMember(listId, user, ref content);
2528 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
2531 if (res == HttpStatusCode.NotFound)
2536 this.CheckStatusCode(res, content);
2540 TwitterUser.ParseJson(content);
2549 public void AddUserToList(string listId, string user)
2556 res = twCon.CreateListMembers(listId, user, ref content);
2560 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
2563 this.CheckStatusCode(res, content);
2566 public void RemoveUserToList(string listId, string user)
2573 res = twCon.DeleteListMembers(listId, user, ref content);
2577 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
2580 this.CheckStatusCode(res, content);
2583 public string CreateHtmlAnchor(string text, List<string> AtList, TwitterEntities entities, List<MediaInfo> media)
2585 if (entities != null)
2587 if (entities.Hashtags != null)
2591 this._hashList.AddRange(entities.Hashtags.Select(x => "#" + x.Text));
2594 if (entities.UserMentions != null)
2596 foreach (var ent in entities.UserMentions)
2598 var screenName = ent.ScreenName.ToLowerInvariant();
2599 if (!AtList.Contains(screenName))
2600 AtList.Add(screenName);
2603 if (entities.Media != null)
2607 foreach (var ent in entities.Media)
2609 if (!media.Any(x => x.Url == ent.MediaUrl))
2611 if (ent.VideoInfo != null &&
2612 ent.Type == "animated_gif" || ent.Type == "video")
2614 //var videoUrl = ent.VideoInfo.Variants
2615 // .Where(v => v.ContentType == "video/mp4")
2616 // .OrderByDescending(v => v.Bitrate)
2617 // .Select(v => v.Url).FirstOrDefault();
2618 media.Add(new MediaInfo(ent.MediaUrl, ent.AltText, ent.ExpandedUrl));
2621 media.Add(new MediaInfo(ent.MediaUrl, ent.AltText, videoUrl: null));
2628 // PostClass.ExpandedUrlInfo を使用して非同期に URL 展開を行うためここでは expanded_url を使用しない
2629 text = TweetFormatter.AutoLinkHtml(text, entities, keepTco: true);
2631 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>");
2632 text = PreProcessUrl(text); //IDN置換
2637 private static readonly Uri SourceUriBase = new Uri("https://twitter.com/");
2640 /// Twitter APIから得たHTML形式のsource文字列を分析し、source名とURLに分離します
2642 public static Tuple<string, Uri> ParseSource(string sourceHtml)
2644 if (string.IsNullOrEmpty(sourceHtml))
2645 return Tuple.Create<string, Uri>("", null);
2650 // sourceHtmlの例: <a href="http://twitter.com" rel="nofollow">Twitter Web Client</a>
2652 var match = Regex.Match(sourceHtml, "^<a href=\"(?<uri>.+?)\".*?>(?<text>.+)</a>$", RegexOptions.IgnoreCase);
2655 sourceText = WebUtility.HtmlDecode(match.Groups["text"].Value);
2658 var uriStr = WebUtility.HtmlDecode(match.Groups["uri"].Value);
2659 sourceUri = new Uri(SourceUriBase, uriStr);
2661 catch (UriFormatException)
2668 sourceText = WebUtility.HtmlDecode(sourceHtml);
2672 return Tuple.Create(sourceText, sourceUri);
2675 public TwitterApiStatus GetInfoApi()
2677 if (Twitter.AccountState != MyCommon.ACCOUNT_STATE.Valid) return null;
2679 if (MyCommon._endingFlag) return null;
2685 res = twCon.RateLimitStatus(ref content);
2689 this.ResetApiStatus();
2693 this.CheckStatusCode(res, content);
2697 MyCommon.TwitterApiInfo.UpdateFromJson(content);
2698 return MyCommon.TwitterApiInfo;
2700 catch (Exception ex)
2702 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
2703 MyCommon.TwitterApiInfo.Reset();
2709 /// ブロック中のユーザーを更新します
2711 /// <exception cref="WebApiException"/>
2712 public void RefreshBlockIds()
2714 if (MyCommon._endingFlag) return;
2717 var newBlockIds = new HashSet<long>();
2720 var ret = this.GetBlockIdsApi(cursor);
2721 newBlockIds.UnionWith(ret.Ids);
2722 cursor = ret.NextCursor;
2723 } while (cursor != 0);
2725 newBlockIds.Remove(this.UserId); // 元のソースにあったので一応残しておく
2727 TabInformations.GetInstance().BlockIds = newBlockIds;
2730 public TwitterIds GetBlockIdsApi(long cursor)
2732 this.CheckAccountState();
2738 res = twCon.GetBlockUserIds(ref content, cursor);
2742 throw new WebApiException("Err:" + e.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", e);
2745 this.CheckStatusCode(res, content);
2749 return TwitterIds.ParseJson(content);
2751 catch(SerializationException e)
2753 var ex = new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, e);
2754 MyCommon.TraceOut(ex);
2759 var ex = new WebApiException("Err:Invalid Json!", content, e);
2760 MyCommon.TraceOut(ex);
2766 /// ミュート中のユーザーIDを更新します
2768 /// <exception cref="WebApiException"/>
2769 public async Task RefreshMuteUserIdsAsync()
2771 if (MyCommon._endingFlag) return;
2773 var ids = await TwitterIds.GetAllItemsAsync(this.GetMuteUserIdsApiAsync)
2774 .ConfigureAwait(false);
2776 TabInformations.GetInstance().MuteUserIds = new HashSet<long>(ids);
2779 public async Task<TwitterIds> GetMuteUserIdsApiAsync(long cursor)
2785 var res = await Task.Run(() => twCon.GetMuteUserIds(ref content, cursor))
2786 .ConfigureAwait(false);
2788 this.CheckStatusCode(res, content);
2790 return TwitterIds.ParseJson(content);
2792 catch (WebException ex)
2794 var ex2 = new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", content, ex);
2795 MyCommon.TraceOut(ex2);
2798 catch (SerializationException ex)
2800 var ex2 = new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
2801 MyCommon.TraceOut(ex2);
2806 public string[] GetHashList()
2811 hashArray = _hashList.ToArray();
2817 public string AccessToken
2821 return twCon.AccessToken;
2825 public string AccessTokenSecret
2829 return twCon.AccessTokenSecret;
2833 private void CheckAccountState()
2835 if (Twitter.AccountState != MyCommon.ACCOUNT_STATE.Valid)
2836 throw new WebApiException("Auth error. Check your account");
2839 private void CheckAccessLevel(TwitterApiAccessLevel accessLevelFlags)
2841 if (!this.AccessLevel.HasFlag(accessLevelFlags))
2842 throw new WebApiException("Auth Err:try to re-authorization.");
2845 private void CheckStatusCode(HttpStatusCode httpStatus, string responseText,
2846 [CallerMemberName] string callerMethodName = "")
2848 if (httpStatus == HttpStatusCode.OK)
2850 Twitter.AccountState = MyCommon.ACCOUNT_STATE.Valid;
2854 if (string.IsNullOrWhiteSpace(responseText))
2856 if (httpStatus == HttpStatusCode.Unauthorized)
2857 Twitter.AccountState = MyCommon.ACCOUNT_STATE.Invalid;
2859 throw new WebApiException("Err:" + httpStatus + "(" + callerMethodName + ")");
2864 var errors = TwitterError.ParseJson(responseText).Errors;
2865 if (errors == null || !errors.Any())
2867 throw new WebApiException("Err:" + httpStatus + "(" + callerMethodName + ")", responseText);
2870 foreach (var error in errors)
2872 if (error.Code == TwitterErrorCode.InvalidToken ||
2873 error.Code == TwitterErrorCode.SuspendedAccount)
2875 Twitter.AccountState = MyCommon.ACCOUNT_STATE.Invalid;
2879 throw new WebApiException("Err:" + string.Join(",", errors.Select(x => x.ToString())) + "(" + callerMethodName + ")", responseText);
2881 catch (SerializationException) { }
2883 throw new WebApiException("Err:" + httpStatus + "(" + callerMethodName + ")", responseText);
2886 public int GetTextLengthRemain(string postText)
2888 var matchDm = Twitter.DMSendTextRegex.Match(postText);
2889 if (matchDm.Success)
2890 return this.GetTextLengthRemainInternal(matchDm.Groups["body"].Value, isDm: true);
2892 return this.GetTextLengthRemainInternal(postText, isDm: false);
2895 private int GetTextLengthRemainInternal(string postText, bool isDm)
2900 while (pos < postText.Length)
2904 if (char.IsSurrogatePair(postText, pos))
2905 pos += 2; // サロゲートペアの場合は2文字分進める
2910 var urls = TweetExtractor.ExtractUrls(postText);
2911 foreach (var url in urls)
2913 var shortUrlLength = url.StartsWith("https://", StringComparison.OrdinalIgnoreCase)
2914 ? this.Configuration.ShortUrlLengthHttps
2915 : this.Configuration.ShortUrlLength;
2917 textLength += shortUrlLength - url.Length;
2921 return this.Configuration.DmTextCharacterLimit - textLength;
2923 return 140 - textLength;
2927 #region "UserStream"
2928 private string trackWord_ = "";
2929 public string TrackWord
2940 private bool allAtReply_ = false;
2941 public bool AllAtReply
2949 allAtReply_ = value;
2953 public event EventHandler NewPostFromStream;
2954 public event EventHandler UserStreamStarted;
2955 public event EventHandler UserStreamStopped;
2956 public event EventHandler<PostDeletedEventArgs> PostDeleted;
2957 public event EventHandler<UserStreamEventReceivedEventArgs> UserStreamEventReceived;
2958 private DateTime _lastUserstreamDataReceived;
2959 private TwitterUserstream userStream;
2961 public class FormattedEvent
2963 public MyCommon.EVENTTYPE Eventtype { get; set; }
2964 public DateTime CreatedAt { get; set; }
2965 public string Event { get; set; }
2966 public string Username { get; set; }
2967 public string Target { get; set; }
2968 public Int64 Id { get; set; }
2969 public bool IsMe { get; set; }
2972 public List<FormattedEvent> storedEvent_ = new List<FormattedEvent>();
2973 public List<FormattedEvent> StoredEvent
2977 return storedEvent_;
2981 storedEvent_ = value;
2985 private readonly IReadOnlyDictionary<string, MyCommon.EVENTTYPE> eventTable = new Dictionary<string, MyCommon.EVENTTYPE>
2987 ["favorite"] = MyCommon.EVENTTYPE.Favorite,
2988 ["unfavorite"] = MyCommon.EVENTTYPE.Unfavorite,
2989 ["follow"] = MyCommon.EVENTTYPE.Follow,
2990 ["list_member_added"] = MyCommon.EVENTTYPE.ListMemberAdded,
2991 ["list_member_removed"] = MyCommon.EVENTTYPE.ListMemberRemoved,
2992 ["block"] = MyCommon.EVENTTYPE.Block,
2993 ["unblock"] = MyCommon.EVENTTYPE.Unblock,
2994 ["user_update"] = MyCommon.EVENTTYPE.UserUpdate,
2995 ["deleted"] = MyCommon.EVENTTYPE.Deleted,
2996 ["list_created"] = MyCommon.EVENTTYPE.ListCreated,
2997 ["list_destroyed"] = MyCommon.EVENTTYPE.ListDestroyed,
2998 ["list_updated"] = MyCommon.EVENTTYPE.ListUpdated,
2999 ["unfollow"] = MyCommon.EVENTTYPE.Unfollow,
3000 ["list_user_subscribed"] = MyCommon.EVENTTYPE.ListUserSubscribed,
3001 ["list_user_unsubscribed"] = MyCommon.EVENTTYPE.ListUserUnsubscribed,
3002 ["mute"] = MyCommon.EVENTTYPE.Mute,
3003 ["unmute"] = MyCommon.EVENTTYPE.Unmute,
3004 ["quoted_tweet"] = MyCommon.EVENTTYPE.QuotedTweet,
3007 public bool IsUserstreamDataReceived
3011 return DateTime.Now.Subtract(this._lastUserstreamDataReceived).TotalSeconds < 31;
3015 private void userStream_StatusArrived(string line)
3017 this._lastUserstreamDataReceived = DateTime.Now;
3018 if (string.IsNullOrEmpty(line)) return;
3020 if (line.First() != '{' || line.Last() != '}')
3022 MyCommon.TraceOut("Invalid JSON (StatusArrived):" + Environment.NewLine + line);
3030 using (var jsonReader = JsonReaderWriterFactory.CreateJsonReader(Encoding.UTF8.GetBytes(line), XmlDictionaryReaderQuotas.Max))
3032 var xElm = XElement.Load(jsonReader);
3033 if (xElm.Element("friends") != null)
3035 Debug.WriteLine("friends");
3038 else if (xElm.Element("delete") != null)
3040 Debug.WriteLine("delete");
3043 if ((idElm = xElm.Element("delete").Element("direct_message")?.Element("id")) != null)
3046 long.TryParse(idElm.Value, out id);
3048 this.PostDeleted?.Invoke(this, new PostDeletedEventArgs(id));
3050 else if ((idElm = xElm.Element("delete").Element("status")?.Element("id")) != null)
3053 long.TryParse(idElm.Value, out id);
3055 this.PostDeleted?.Invoke(this, new PostDeletedEventArgs(id));
3059 MyCommon.TraceOut("delete:" + line);
3062 for (int i = this.StoredEvent.Count - 1; i >= 0; i--)
3064 var sEvt = this.StoredEvent[i];
3065 if (sEvt.Id == id && (sEvt.Event == "favorite" || sEvt.Event == "unfavorite"))
3067 this.StoredEvent.RemoveAt(i);
3072 else if (xElm.Element("limit") != null)
3074 Debug.WriteLine(line);
3077 else if (xElm.Element("event") != null)
3079 Debug.WriteLine("event: " + xElm.Element("event").Value);
3080 CreateEventFromJson(line);
3083 else if (xElm.Element("direct_message") != null)
3085 Debug.WriteLine("direct_message");
3088 else if (xElm.Element("retweeted_status") != null)
3090 var sourceUserId = xElm.XPathSelectElement("/user/id_str").Value;
3091 var targetUserId = xElm.XPathSelectElement("/retweeted_status/user/id_str").Value;
3093 // 自分に関係しないリツイートの場合は無視する
3094 var selfUserId = this.UserId.ToString();
3095 if (sourceUserId == selfUserId || targetUserId == selfUserId)
3097 // 公式 RT をイベントとしても扱う
3098 var evt = CreateEventFromRetweet(xElm);
3101 this.StoredEvent.Insert(0, evt);
3103 this.UserStreamEventReceived?.Invoke(this, new UserStreamEventReceivedEventArgs(evt));
3107 // 従来通り公式 RT の表示も行うため return しない
3109 else if (xElm.Element("scrub_geo") != null)
3113 TabInformations.GetInstance().ScrubGeoReserve(long.Parse(xElm.Element("scrub_geo").Element("user_id").Value),
3114 long.Parse(xElm.Element("scrub_geo").Element("up_to_status_id").Value));
3118 MyCommon.TraceOut("scrub_geo:" + line);
3126 CreateDirectMessagesFromJson(line, MyCommon.WORKERTYPE.UserStream, false);
3130 CreatePostsFromJson("[" + line + "]", MyCommon.WORKERTYPE.Timeline, null, false);
3133 catch (WebApiException ex)
3135 MyCommon.TraceOut(ex);
3138 catch(NullReferenceException)
3140 MyCommon.TraceOut("NullRef StatusArrived: " + line);
3143 this.NewPostFromStream?.Invoke(this, EventArgs.Empty);
3147 /// UserStreamsから受信した公式RTをイベントに変換します
3149 private FormattedEvent CreateEventFromRetweet(XElement xElm)
3151 return new FormattedEvent
3153 Eventtype = MyCommon.EVENTTYPE.Retweet,
3155 CreatedAt = MyCommon.DateTimeParse(xElm.XPathSelectElement("/created_at").Value),
3156 IsMe = xElm.XPathSelectElement("/user/id_str").Value == this.UserId.ToString(),
3157 Username = xElm.XPathSelectElement("/user/screen_name").Value,
3158 Target = string.Format("@{0}:{1}", new[]
3160 xElm.XPathSelectElement("/retweeted_status/user/screen_name").Value,
3161 WebUtility.HtmlDecode(xElm.XPathSelectElement("/retweeted_status/text").Value),
3163 Id = long.Parse(xElm.XPathSelectElement("/retweeted_status/id_str").Value),
3167 private void CreateEventFromJson(string content)
3169 TwitterStreamEvent eventData = null;
3172 eventData = TwitterStreamEvent.ParseJson(content);
3174 catch(SerializationException ex)
3176 MyCommon.TraceOut(ex, "Event Serialize Exception!" + Environment.NewLine + content);
3180 MyCommon.TraceOut(ex, "Event Exception!" + Environment.NewLine + content);
3183 var evt = new FormattedEvent();
3184 evt.CreatedAt = MyCommon.DateTimeParse(eventData.CreatedAt);
3185 evt.Event = eventData.Event;
3186 evt.Username = eventData.Source.ScreenName;
3187 evt.IsMe = evt.Username.ToLowerInvariant().Equals(this.Username.ToLowerInvariant());
3189 MyCommon.EVENTTYPE eventType;
3190 eventTable.TryGetValue(eventData.Event, out eventType);
3191 evt.Eventtype = eventType;
3193 TwitterStreamEvent<TwitterStatus> tweetEvent;
3195 switch (eventData.Event)
3197 case "access_revoked":
3198 case "access_unrevoked":
3200 case "user_suspend":
3203 if (eventData.Target.ScreenName.ToLowerInvariant().Equals(_uname))
3205 if (!this.followerId.Contains(eventData.Source.Id)) this.followerId.Add(eventData.Source.Id);
3209 return; //Block後のUndoをすると、SourceとTargetが逆転したfollowイベントが帰ってくるため。
3214 evt.Target = "@" + eventData.Target.ScreenName;
3216 case "favorited_retweet":
3217 case "retweeted_retweet":
3221 tweetEvent = TwitterStreamEvent<TwitterStatus>.ParseJson(content);
3222 evt.Target = "@" + tweetEvent.TargetObject.User.ScreenName + ":" + WebUtility.HtmlDecode(tweetEvent.TargetObject.Text);
3223 evt.Id = tweetEvent.TargetObject.Id;
3225 if (SettingCommon.Instance.IsRemoveSameEvent)
3227 if (this.StoredEvent.Any(ev => ev.Username == evt.Username && ev.Eventtype == evt.Eventtype && ev.Target == evt.Target))
3231 var tabinfo = TabInformations.GetInstance();
3234 var statusId = tweetEvent.TargetObject.Id;
3235 if (!tabinfo.Posts.TryGetValue(statusId, out post))
3238 if (eventData.Event == "favorite")
3240 var favTab = tabinfo.GetTabByType(MyCommon.TabUsageType.Favorites);
3241 if (!favTab.Contains(post.StatusId))
3242 favTab.AddPostImmediately(post.StatusId, post.IsRead);
3244 if (tweetEvent.Source.Id == this.UserId)
3248 else if (tweetEvent.Target.Id == this.UserId)
3250 post.FavoritedCount++;
3252 if (SettingCommon.Instance.FavEventUnread)
3253 tabinfo.SetReadAllTab(post.StatusId, read: false);
3258 if (tweetEvent.Source.Id == this.UserId)
3262 else if (tweetEvent.Target.Id == this.UserId)
3264 post.FavoritedCount = Math.Max(0, post.FavoritedCount - 1);
3268 case "quoted_tweet":
3269 if (evt.IsMe) return;
3271 tweetEvent = TwitterStreamEvent<TwitterStatus>.ParseJson(content);
3272 evt.Target = "@" + tweetEvent.TargetObject.User.ScreenName + ":" + WebUtility.HtmlDecode(tweetEvent.TargetObject.Text);
3273 evt.Id = tweetEvent.TargetObject.Id;
3275 if (SettingCommon.Instance.IsRemoveSameEvent)
3277 if (this.StoredEvent.Any(ev => ev.Username == evt.Username && ev.Eventtype == evt.Eventtype && ev.Target == evt.Target))
3281 case "list_member_added":
3282 case "list_member_removed":
3283 case "list_created":
3284 case "list_destroyed":
3285 case "list_updated":
3286 case "list_user_subscribed":
3287 case "list_user_unsubscribed":
3288 var listEvent = TwitterStreamEvent<TwitterList>.ParseJson(content);
3289 evt.Target = listEvent.TargetObject.FullName;
3292 if (!TabInformations.GetInstance().BlockIds.Contains(eventData.Target.Id)) TabInformations.GetInstance().BlockIds.Add(eventData.Target.Id);
3296 if (TabInformations.GetInstance().BlockIds.Contains(eventData.Target.Id)) TabInformations.GetInstance().BlockIds.Remove(eventData.Target.Id);
3305 evt.Target = "@" + eventData.Target.ScreenName;
3306 if (!TabInformations.GetInstance().MuteUserIds.Contains(eventData.Target.Id))
3308 TabInformations.GetInstance().MuteUserIds.Add(eventData.Target.Id);
3312 evt.Target = "@" + eventData.Target.ScreenName;
3313 if (TabInformations.GetInstance().MuteUserIds.Contains(eventData.Target.Id))
3315 TabInformations.GetInstance().MuteUserIds.Remove(eventData.Target.Id);
3320 MyCommon.TraceOut("Unknown Event:" + evt.Event + Environment.NewLine + content);
3323 this.StoredEvent.Insert(0, evt);
3325 this.UserStreamEventReceived?.Invoke(this, new UserStreamEventReceivedEventArgs(evt));
3328 private void userStream_Started()
3330 this.UserStreamStarted?.Invoke(this, EventArgs.Empty);
3333 private void userStream_Stopped()
3335 this.UserStreamStopped?.Invoke(this, EventArgs.Empty);
3338 public bool UserStreamEnabled
3342 return userStream == null ? false : userStream.Enabled;
3346 public void StartUserStream()
3348 if (userStream != null)
3352 userStream = new TwitterUserstream(twCon);
3353 userStream.StatusArrived += userStream_StatusArrived;
3354 userStream.Started += userStream_Started;
3355 userStream.Stopped += userStream_Stopped;
3356 userStream.Start(this.AllAtReply, this.TrackWord);
3359 public void StopUserStream()
3361 userStream?.Dispose();
3363 if (!MyCommon._endingFlag)
3365 this.UserStreamStopped?.Invoke(this, EventArgs.Empty);
3369 public void ReconnectUserStream()
3371 if (userStream != null)
3373 this.StartUserStream();
3377 private class TwitterUserstream : IDisposable
3379 public event Action<string> StatusArrived;
3380 public event Action Stopped;
3381 public event Action Started;
3382 private HttpTwitter twCon;
3384 private Thread _streamThread;
3385 private bool _streamActive;
3387 private bool _allAtreplies = false;
3388 private string _trackwords = "";
3390 public TwitterUserstream(HttpTwitter twitterConnection)
3392 twCon = (HttpTwitter)twitterConnection.Clone();
3395 public void Start(bool allAtReplies, string trackwords)
3397 this.AllAtReplies = allAtReplies;
3398 this.TrackWords = trackwords;
3399 _streamActive = true;
3400 if (_streamThread != null && _streamThread.IsAlive) return;
3401 _streamThread = new Thread(UserStreamLoop);
3402 _streamThread.Name = "UserStreamReceiver";
3403 _streamThread.IsBackground = true;
3404 _streamThread.Start();
3411 return _streamActive;
3415 public bool AllAtReplies
3419 return _allAtreplies;
3423 _allAtreplies = value;
3427 public string TrackWords
3435 _trackwords = value;
3439 private void UserStreamLoop()
3445 StreamReader sr = null;
3448 if (!MyCommon.IsNetworkAvailable())
3456 var res = twCon.UserStream(ref st, _allAtreplies, _trackwords, Networking.GetUserAgentString());
3460 case HttpStatusCode.OK:
3461 Twitter.AccountState = MyCommon.ACCOUNT_STATE.Valid;
3463 case HttpStatusCode.Unauthorized:
3464 Twitter.AccountState = MyCommon.ACCOUNT_STATE.Invalid;
3472 //MyCommon.TraceOut("Stop:stream is null")
3476 sr = new StreamReader(st);
3478 while (_streamActive && !sr.EndOfStream && Twitter.AccountState == MyCommon.ACCOUNT_STATE.Valid)
3480 StatusArrived?.Invoke(sr.ReadLine());
3481 //this.LastTime = Now;
3484 if (sr.EndOfStream || Twitter.AccountState == MyCommon.ACCOUNT_STATE.Invalid)
3487 //MyCommon.TraceOut("Stop:EndOfStream")
3492 catch(WebException ex)
3494 if (ex.Status == WebExceptionStatus.Timeout)
3496 sleepSec = 30; //MyCommon.TraceOut("Stop:Timeout")
3498 else if (ex.Response != null && (int)((HttpWebResponse)ex.Response).StatusCode == 420)
3500 //MyCommon.TraceOut("Stop:Connection Limit")
3506 //MyCommon.TraceOut("Stop:WebException " + ex.Status.ToString())
3509 catch(ThreadAbortException)
3516 //MyCommon.TraceOut("Stop:IOException with Active." + Environment.NewLine + ex.Message)
3518 catch(ArgumentException ex)
3520 //System.ArgumentException: ストリームを読み取れませんでした。
3521 //サーバー側もしくは通信経路上で切断された場合?タイムアウト頻発後発生
3523 MyCommon.TraceOut(ex, "Stop:ArgumentException");
3527 MyCommon.TraceOut("Stop:Exception." + Environment.NewLine + ex.Message);
3528 MyCommon.ExceptionOut(ex);
3537 twCon.RequestAbort();
3542 while (_streamActive && ms < sleepSec * 1000)
3550 } while (this._streamActive);
3556 MyCommon.TraceOut("Stop:EndLoop");
3559 #region "IDisposable Support"
3560 private bool disposedValue; // 重複する呼び出しを検出するには
3563 protected virtual void Dispose(bool disposing)
3565 if (!this.disposedValue)
3569 _streamActive = false;
3570 if (_streamThread != null && _streamThread.IsAlive)
3572 _streamThread.Abort();
3576 this.disposedValue = true;
3579 //protected Overrides void Finalize()
3581 // // このコードを変更しないでください。クリーンアップ コードを上の Dispose(bool disposing) に記述します。
3583 // MyBase.Finalize()
3586 // このコードは、破棄可能なパターンを正しく実装できるように Visual Basic によって追加されました。
3587 public void Dispose()
3589 // このコードを変更しないでください。クリーンアップ コードを上の Dispose(bool disposing) に記述します。
3591 GC.SuppressFinalize(this);
3598 #region "IDisposable Support"
3599 private bool disposedValue; // 重複する呼び出しを検出するには
3602 protected virtual void Dispose(bool disposing)
3604 if (!this.disposedValue)
3608 this.StopUserStream();
3611 this.disposedValue = true;
3614 //protected Overrides void Finalize()
3616 // // このコードを変更しないでください。クリーンアップ コードを上の Dispose(bool disposing) に記述します。
3618 // MyBase.Finalize()
3621 // このコードは、破棄可能なパターンを正しく実装できるように Visual Basic によって追加されました。
3622 public void Dispose()
3624 // このコードを変更しないでください。クリーンアップ コードを上の Dispose(bool disposing) に記述します。
3626 GC.SuppressFinalize(this);
3631 public class PostDeletedEventArgs : EventArgs
3633 public long StatusId { get; }
3635 public PostDeletedEventArgs(long statusId)
3637 this.StatusId = statusId;
3641 public class UserStreamEventReceivedEventArgs : EventArgs
3643 public Twitter.FormattedEvent EventData { get; }
3645 public UserStreamEventReceivedEventArgs(Twitter.FormattedEvent eventData)
3647 this.EventData = eventData;