OSDN Git Service

Twitter.GetTimelineApiメソッドをGetHomeTimelineApi, GetMentionsTimelineApiに分割
[opentween/open-tween.git] / OpenTween / Twitter.cs
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.
10 //
11 // This file is part of OpenTween.
12 //
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)
16 // any later version.
17 //
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
21 // for more details.
22 //
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.
27
28 using System.Diagnostics;
29 using System.IO;
30 using System.Linq;
31 using System.Net;
32 using System.Net.Http;
33 using System.Runtime.CompilerServices;
34 using System.Runtime.Serialization;
35 using System.Runtime.Serialization.Json;
36 using System.Text;
37 using System.Text.RegularExpressions;
38 using System.Threading;
39 using System.Threading.Tasks;
40 using System.Web;
41 using System.Xml;
42 using System.Xml.Linq;
43 using System.Xml.XPath;
44 using System;
45 using System.Reflection;
46 using System.Collections.Generic;
47 using System.Drawing;
48 using System.Windows.Forms;
49 using OpenTween.Api;
50 using OpenTween.Api.DataModel;
51 using OpenTween.Connection;
52 using OpenTween.Models;
53
54 namespace OpenTween
55 {
56     public class Twitter : IDisposable
57     {
58         #region Regexp from twitter-text-js
59
60         // The code in this region code block incorporates works covered by
61         // the following copyright and permission notices:
62         //
63         //   Copyright 2011 Twitter, Inc.
64         //
65         //   Licensed under the Apache License, Version 2.0 (the "License"); you
66         //   may not use this work except in compliance with the License. You
67         //   may obtain a copy of the License in the LICENSE file, or at:
68         //
69         //   http://www.apache.org/licenses/LICENSE-2.0
70         //
71         //   Unless required by applicable law or agreed to in writing, software
72         //   distributed under the License is distributed on an "AS IS" BASIS,
73         //   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
74         //   implied. See the License for the specific language governing
75         //   permissions and limitations under the License.
76
77         //Hashtag用正規表現
78         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";
79         private const string NON_LATIN_HASHTAG_CHARS = @"\u0400-\u04ff\u0500-\u0527\u1100-\u11ff\u3130-\u3185\uA960-\uA97F\uAC00-\uD7AF\uD7B0-\uD7FF";
80         //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";
81         private const string CJ_HASHTAG_CHARACTERS = @"\u30A1-\u30FA\u30FC\u3005\uFF66-\uFF9F\uFF10-\uFF19\uFF21-\uFF3A\uFF41-\uFF5A\u3041-\u309A\u3400-\u4DBF\p{IsCJKUnifiedIdeographs}";
82         private const string HASHTAG_BOUNDARY = @"^|$|\s|「|」|。|\.|!";
83         private const string HASHTAG_ALPHA = "[a-z_" + LATIN_ACCENTS + NON_LATIN_HASHTAG_CHARS + CJ_HASHTAG_CHARACTERS + "]";
84         private const string HASHTAG_ALPHANUMERIC = "[a-z0-9_" + LATIN_ACCENTS + NON_LATIN_HASHTAG_CHARS + CJ_HASHTAG_CHARACTERS + "]";
85         private const string HASHTAG_TERMINATOR = "[^a-z0-9_" + LATIN_ACCENTS + NON_LATIN_HASHTAG_CHARS + CJ_HASHTAG_CHARACTERS + "]";
86         public const string HASHTAG = "(" + HASHTAG_BOUNDARY + ")(#|#)(" + HASHTAG_ALPHANUMERIC + "*" + HASHTAG_ALPHA + HASHTAG_ALPHANUMERIC + "*)(?=" + HASHTAG_TERMINATOR + "|" + HASHTAG_BOUNDARY + ")";
87         //URL正規表現
88         private const string url_valid_preceding_chars = @"(?:[^A-Za-z0-9@@$##\ufffe\ufeff\uffff\u202a-\u202e]|^)";
89         public const string url_invalid_without_protocol_preceding_chars = @"[-_./]$";
90         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";
91         private const string url_valid_domain_chars = @"[^" + url_invalid_domain_chars + "]";
92         private const string url_valid_subdomain = @"(?:(?:" + url_valid_domain_chars + @"(?:[_-]|" + url_valid_domain_chars + @")*)?" + url_valid_domain_chars + @"\.)";
93         private const string url_valid_domain_name = @"(?:(?:" + url_valid_domain_chars + @"(?:-|" + url_valid_domain_chars + @")*)?" + url_valid_domain_chars + @"\.)";
94         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]|$))";
95         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]|$))";
96         private const string url_valid_punycode = @"(?:xn--[0-9a-z]+)";
97         private const string url_valid_domain = @"(?<domain>" + url_valid_subdomain + "*" + url_valid_domain_name + "(?:" + url_valid_GTLD + "|" + url_valid_CCTLD + ")|" + url_valid_punycode + ")";
98         public const string url_valid_ascii_domain = @"(?:(?:[a-z0-9" + LATIN_ACCENTS + @"]+)\.)+(?:" + url_valid_GTLD + "|" + url_valid_CCTLD + "|" + url_valid_punycode + ")";
99         public const string url_invalid_short_domain = "^" + url_valid_domain_name + url_valid_CCTLD + "$";
100         private const string url_valid_port_number = @"[0-9]+";
101
102         private const string url_valid_general_path_chars = @"[a-z0-9!*';:=+,.$/%#\[\]\-_~|&" + LATIN_ACCENTS + "]";
103         private const string url_balance_parens = @"(?:\(" + url_valid_general_path_chars + @"+\))";
104         private const string url_valid_path_ending_chars = @"(?:[+\-a-z0-9=_#/" + LATIN_ACCENTS + "]|" + url_balance_parens + ")";
105         private const string pth = "(?:" +
106             "(?:" +
107                 url_valid_general_path_chars + "*" +
108                 "(?:" + url_balance_parens + url_valid_general_path_chars + "*)*" +
109                 url_valid_path_ending_chars +
110                 ")|(?:@" + url_valid_general_path_chars + "+/)" +
111             ")";
112         private const string qry = @"(?<query>\?[a-z0-9!?*'();:&=+$/%#\[\]\-_.,~|]*[a-z0-9_&=#/])?";
113         public const string rgUrl = @"(?<before>" + url_valid_preceding_chars + ")" +
114                                     "(?<url>(?<protocol>https?://)?" +
115                                     "(?<domain>" + url_valid_domain + ")" +
116                                     "(?::" + url_valid_port_number + ")?" +
117                                     "(?<path>/" + pth + "*)?" +
118                                     qry +
119                                     ")";
120
121         #endregion
122
123         /// <summary>
124         /// Twitter API のステータスページのURL
125         /// </summary>
126         public const string ServiceAvailabilityStatusUrl = "https://status.io.watchmouse.com/7617";
127
128         /// <summary>
129         /// ツイートへのパーマリンクURLを判定する正規表現
130         /// </summary>
131         public static readonly Regex StatusUrlRegex = new Regex(@"https?://([^.]+\.)?twitter\.com/(#!/)?(?<ScreenName>[a-zA-Z0-9_]+)/status(es)?/(?<StatusId>[0-9]+)(/photo)?", RegexOptions.IgnoreCase);
132
133         /// <summary>
134         /// FavstarやaclogなどTwitter関連サービスのパーマリンクURLからステータスIDを抽出する正規表現
135         /// </summary>
136         public static readonly Regex ThirdPartyStatusUrlRegex = new Regex(@"https?://(?:[^.]+\.)?(?:
137   favstar\.fm/users/[a-zA-Z0-9_]+/status/       # Favstar
138 | favstar\.fm/t/                                # Favstar (short)
139 | aclog\.koba789\.com/i/                        # aclog
140 | frtrt\.net/solo_status\.php\?status=          # RtRT
141 )(?<StatusId>[0-9]+)", RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace);
142
143         /// <summary>
144         /// DM送信かどうかを判定する正規表現
145         /// </summary>
146         public static readonly Regex DMSendTextRegex = new Regex(@"^DM? +(?<id>[a-zA-Z0-9_]+) +(?<body>.*)", RegexOptions.IgnoreCase | RegexOptions.Singleline);
147
148         public TwitterApi Api { get; }
149         public TwitterConfiguration Configuration { get; private set; }
150
151         delegate void GetIconImageDelegate(PostClass post);
152         private readonly object LockObj = new object();
153         private ISet<long> followerId = new HashSet<long>();
154         private bool _GetFollowerResult = false;
155         private long[] noRTId = new long[0];
156         private bool _GetNoRetweetResult = false;
157
158         //プロパティからアクセスされる共通情報
159         private string _uname;
160
161         private bool _readOwnPost;
162         private List<string> _hashList = new List<string>();
163
164         //max_idで古い発言を取得するために保持(lists分は個別タブで管理)
165         private long minDirectmessage = long.MaxValue;
166         private long minDirectmessageSent = long.MaxValue;
167
168         //private FavoriteQueue favQueue;
169
170         //private List<PostClass> _deletemessages = new List<PostClass>();
171
172         public Twitter() : this(new TwitterApi())
173         {
174         }
175
176         public Twitter(TwitterApi api)
177         {
178             this.Api = api;
179             this.Configuration = TwitterConfiguration.DefaultConfiguration();
180         }
181
182         public TwitterApiAccessLevel AccessLevel
183         {
184             get
185             {
186                 return MyCommon.TwitterApiInfo.AccessLevel;
187             }
188         }
189
190         protected void ResetApiStatus()
191         {
192             MyCommon.TwitterApiInfo.Reset();
193         }
194
195         public void ClearAuthInfo()
196         {
197             Twitter.AccountState = MyCommon.ACCOUNT_STATE.Invalid;
198             this.ResetApiStatus();
199         }
200
201         [Obsolete]
202         public void VerifyCredentials()
203         {
204             try
205             {
206                 this.VerifyCredentialsAsync().Wait();
207             }
208             catch (AggregateException ex) when (ex.InnerException is WebApiException)
209             {
210                 throw new WebApiException(ex.InnerException.Message, ex);
211             }
212         }
213
214         public async Task VerifyCredentialsAsync()
215         {
216             var user = await this.Api.AccountVerifyCredentials()
217                 .ConfigureAwait(false);
218
219             this.UpdateUserStats(user);
220         }
221
222         public void Initialize(string token, string tokenSecret, string username, long userId)
223         {
224             //OAuth認証
225             if (string.IsNullOrEmpty(token) || string.IsNullOrEmpty(tokenSecret) || string.IsNullOrEmpty(username))
226             {
227                 Twitter.AccountState = MyCommon.ACCOUNT_STATE.Invalid;
228             }
229             this.ResetApiStatus();
230             this.Api.Initialize(token, tokenSecret, userId, username);
231             _uname = username.ToLowerInvariant();
232             if (SettingCommon.Instance.UserstreamStartup) this.ReconnectUserStream();
233         }
234
235         public string PreProcessUrl(string orgData)
236         {
237             int posl1;
238             var posl2 = 0;
239             //var IDNConveter = new IdnMapping();
240             var href = "<a href=\"";
241
242             while (true)
243             {
244                 if (orgData.IndexOf(href, posl2, StringComparison.Ordinal) > -1)
245                 {
246                     var urlStr = "";
247                     // IDN展開
248                     posl1 = orgData.IndexOf(href, posl2, StringComparison.Ordinal);
249                     posl1 += href.Length;
250                     posl2 = orgData.IndexOf("\"", posl1, StringComparison.Ordinal);
251                     urlStr = orgData.Substring(posl1, posl2 - posl1);
252
253                     if (!urlStr.StartsWith("http://", StringComparison.Ordinal)
254                         && !urlStr.StartsWith("https://", StringComparison.Ordinal)
255                         && !urlStr.StartsWith("ftp://", StringComparison.Ordinal))
256                     {
257                         continue;
258                     }
259
260                     var replacedUrl = MyCommon.IDNEncode(urlStr);
261                     if (replacedUrl == null) continue;
262                     if (replacedUrl == urlStr) continue;
263
264                     orgData = orgData.Replace("<a href=\"" + urlStr, "<a href=\"" + replacedUrl);
265                     posl2 = 0;
266                 }
267                 else
268                 {
269                     break;
270                 }
271             }
272             return orgData;
273         }
274
275         private string GetPlainText(string orgData)
276         {
277             return WebUtility.HtmlDecode(Regex.Replace(orgData, "(?<tagStart><a [^>]+>)(?<text>[^<]+)(?<tagEnd></a>)", "${text}"));
278         }
279
280         // htmlの簡易サニタイズ(詳細表示に不要なタグの除去)
281
282         private string SanitizeHtml(string orgdata)
283         {
284             var retdata = orgdata;
285
286             retdata = Regex.Replace(retdata, "<(script|object|applet|image|frameset|fieldset|legend|style).*" +
287                 "</(script|object|applet|image|frameset|fieldset|legend|style)>", "", RegexOptions.IgnoreCase);
288
289             retdata = Regex.Replace(retdata, "<(frame|link|iframe|img)>", "", RegexOptions.IgnoreCase);
290
291             return retdata;
292         }
293
294         private string AdjustHtml(string orgData)
295         {
296             var retStr = orgData;
297             //var m = Regex.Match(retStr, "<a [^>]+>[#|#](?<1>[a-zA-Z0-9_]+)</a>");
298             //while (m.Success)
299             //{
300             //    lock (LockObj)
301             //    {
302             //        _hashList.Add("#" + m.Groups(1).Value);
303             //    }
304             //    m = m.NextMatch;
305             //}
306             retStr = Regex.Replace(retStr, "<a [^>]*href=\"/", "<a href=\"https://twitter.com/");
307             retStr = retStr.Replace("<a href=", "<a target=\"_self\" href=");
308             retStr = Regex.Replace(retStr, @"(\r\n?|\n)", "<br>"); // CRLF, CR, LF は全て <br> に置換する
309
310             //半角スペースを置換(Thanks @anis774)
311             var ret = false;
312             do
313             {
314                 ret = EscapeSpace(ref retStr);
315             } while (!ret);
316
317             return SanitizeHtml(retStr);
318         }
319
320         private bool EscapeSpace(ref string html)
321         {
322             //半角スペースを置換(Thanks @anis774)
323             var isTag = false;
324             for (int i = 0; i < html.Length; i++)
325             {
326                 if (html[i] == '<')
327                 {
328                     isTag = true;
329                 }
330                 if (html[i] == '>')
331                 {
332                     isTag = false;
333                 }
334
335                 if ((!isTag) && (html[i] == ' '))
336                 {
337                     html = html.Remove(i, 1);
338                     html = html.Insert(i, "&nbsp;");
339                     return false;
340                 }
341             }
342             return true;
343         }
344
345         private struct PostInfo
346         {
347             public string CreatedAt;
348             public string Id;
349             public string Text;
350             public string UserId;
351             public PostInfo(string Created, string IdStr, string txt, string uid)
352             {
353                 CreatedAt = Created;
354                 Id = IdStr;
355                 Text = txt;
356                 UserId = uid;
357             }
358             public bool Equals(PostInfo dst)
359             {
360                 if (this.CreatedAt == dst.CreatedAt && this.Id == dst.Id && this.Text == dst.Text && this.UserId == dst.UserId)
361                 {
362                     return true;
363                 }
364                 else
365                 {
366                     return false;
367                 }
368             }
369         }
370
371         static private PostInfo _prev = new PostInfo("", "", "", "");
372         private bool IsPostRestricted(TwitterStatus status)
373         {
374             var _current = new PostInfo("", "", "", "");
375
376             _current.CreatedAt = status.CreatedAt;
377             _current.Id = status.IdStr;
378             if (status.Text == null)
379             {
380                 _current.Text = "";
381             }
382             else
383             {
384                 _current.Text = status.Text;
385             }
386             _current.UserId = status.User.IdStr;
387
388             if (_current.Equals(_prev))
389             {
390                 return true;
391             }
392             _prev.CreatedAt = _current.CreatedAt;
393             _prev.Id = _current.Id;
394             _prev.Text = _current.Text;
395             _prev.UserId = _current.UserId;
396
397             return false;
398         }
399
400         public async Task PostStatus(string postStr, long? reply_to, IReadOnlyList<long> mediaIds = null)
401         {
402             this.CheckAccountState();
403
404             if (mediaIds == null &&
405                 Twitter.DMSendTextRegex.IsMatch(postStr))
406             {
407                 await this.SendDirectMessage(postStr)
408                     .ConfigureAwait(false);
409                 return;
410             }
411
412             var response = await this.Api.StatusesUpdate(postStr, reply_to, mediaIds)
413                 .ConfigureAwait(false);
414
415             var status = await response.LoadJsonAsync()
416                 .ConfigureAwait(false);
417
418             this.UpdateUserStats(status.User);
419
420             if (IsPostRestricted(status))
421             {
422                 throw new WebApiException("OK:Delaying?");
423             }
424         }
425
426         public async Task PostStatusWithMultipleMedia(string postStr, long? reply_to, IMediaItem[] mediaItems)
427         {
428             this.CheckAccountState();
429
430             if (Twitter.DMSendTextRegex.IsMatch(postStr))
431             {
432                 await this.SendDirectMessage(postStr)
433                     .ConfigureAwait(false);
434                 return;
435             }
436
437             if (mediaItems.Length == 0)
438                 throw new WebApiException("Err:Invalid Files!");
439
440             var uploadTasks = from m in mediaItems
441                               select this.UploadMedia(m);
442
443             var mediaIds = await Task.WhenAll(uploadTasks)
444                 .ConfigureAwait(false);
445
446             await this.PostStatus(postStr, reply_to, mediaIds)
447                 .ConfigureAwait(false);
448         }
449
450         public async Task<long> UploadMedia(IMediaItem item)
451         {
452             this.CheckAccountState();
453
454             var response = await this.Api.MediaUpload(item)
455                 .ConfigureAwait(false);
456
457             var media = await response.LoadJsonAsync()
458                 .ConfigureAwait(false);
459
460             return media.MediaId;
461         }
462
463         public async Task SendDirectMessage(string postStr)
464         {
465             this.CheckAccountState();
466             this.CheckAccessLevel(TwitterApiAccessLevel.ReadWriteAndDirectMessage);
467
468             var mc = Twitter.DMSendTextRegex.Match(postStr);
469
470             var response = await this.Api.DirectMessagesNew(mc.Groups["body"].Value, mc.Groups["id"].Value)
471                 .ConfigureAwait(false);
472
473             var dm = await response.LoadJsonAsync()
474                 .ConfigureAwait(false);
475
476             this.UpdateUserStats(dm.Sender);
477         }
478
479         public async Task PostRetweet(long id, bool read)
480         {
481             this.CheckAccountState();
482
483             //データ部分の生成
484             var target = id;
485             var post = TabInformations.GetInstance()[id];
486             if (post == null)
487             {
488                 throw new WebApiException("Err:Target isn't found.");
489             }
490             if (TabInformations.GetInstance()[id].RetweetedId != null)
491             {
492                 target = TabInformations.GetInstance()[id].RetweetedId.Value; //再RTの場合は元発言をRT
493             }
494
495             var response = await this.Api.StatusesRetweet(target)
496                 .ConfigureAwait(false);
497
498             var status = await response.LoadJsonAsync()
499                 .ConfigureAwait(false);
500
501             //ReTweetしたものをTLに追加
502             post = CreatePostsFromStatusData(status);
503             if (post == null)
504                 throw new WebApiException("Invalid Json!");
505
506             //二重取得回避
507             lock (LockObj)
508             {
509                 if (TabInformations.GetInstance().ContainsKey(post.StatusId))
510                     return;
511             }
512             //Retweet判定
513             if (post.RetweetedId == null)
514                 throw new WebApiException("Invalid Json!");
515             //ユーザー情報
516             post.IsMe = true;
517
518             post.IsRead = read;
519             post.IsOwl = false;
520             if (_readOwnPost) post.IsRead = true;
521             post.IsDm = false;
522
523             TabInformations.GetInstance().AddPost(post);
524         }
525
526         public string Username
527             => this.Api.CurrentScreenName;
528
529         public long UserId
530             => this.Api.CurrentUserId;
531
532         private static MyCommon.ACCOUNT_STATE _accountState = MyCommon.ACCOUNT_STATE.Valid;
533         public static MyCommon.ACCOUNT_STATE AccountState
534         {
535             get
536             {
537                 return _accountState;
538             }
539             set
540             {
541                 _accountState = value;
542             }
543         }
544
545         public bool RestrictFavCheck { get; set; }
546
547 #region "バージョンアップ"
548         public void GetTweenBinary(string strVer)
549         {
550             try
551             {
552                 //本体
553                 if (!(new HttpVarious()).GetDataToFile("http://tween.sourceforge.jp/Tween" + strVer + ".gz?" + DateTime.Now.ToString("yyMMddHHmmss") + Environment.TickCount.ToString(),
554                                                     Path.Combine(MyCommon.settingPath, "TweenNew.exe")))
555                 {
556                     throw new WebApiException("Err:Download failed");
557                 }
558                 //英語リソース
559                 if (!Directory.Exists(Path.Combine(MyCommon.settingPath, "en")))
560                 {
561                     Directory.CreateDirectory(Path.Combine(MyCommon.settingPath, "en"));
562                 }
563                 if (!(new HttpVarious()).GetDataToFile("http://tween.sourceforge.jp/TweenResEn" + strVer + ".gz?" + DateTime.Now.ToString("yyMMddHHmmss") + Environment.TickCount.ToString(),
564                                                     Path.Combine(Path.Combine(MyCommon.settingPath, "en"), "Tween.resourcesNew.dll")))
565                 {
566                     throw new WebApiException("Err:Download failed");
567                 }
568                 //その他言語圏のリソース。取得失敗しても継続
569                 //UIの言語圏のリソース
570                 var curCul = "";
571                 if (!Thread.CurrentThread.CurrentUICulture.IsNeutralCulture)
572                 {
573                     var idx = Thread.CurrentThread.CurrentUICulture.Name.LastIndexOf('-');
574                     if (idx > -1)
575                     {
576                         curCul = Thread.CurrentThread.CurrentUICulture.Name.Substring(0, idx);
577                     }
578                     else
579                     {
580                         curCul = Thread.CurrentThread.CurrentUICulture.Name;
581                     }
582                 }
583                 else
584                 {
585                     curCul = Thread.CurrentThread.CurrentUICulture.Name;
586                 }
587                 if (!string.IsNullOrEmpty(curCul) && curCul != "en" && curCul != "ja")
588                 {
589                     if (!Directory.Exists(Path.Combine(MyCommon.settingPath, curCul)))
590                     {
591                         Directory.CreateDirectory(Path.Combine(MyCommon.settingPath, curCul));
592                     }
593                     if (!(new HttpVarious()).GetDataToFile("http://tween.sourceforge.jp/TweenRes" + curCul + strVer + ".gz?" + DateTime.Now.ToString("yyMMddHHmmss") + Environment.TickCount.ToString(),
594                                                         Path.Combine(Path.Combine(MyCommon.settingPath, curCul), "Tween.resourcesNew.dll")))
595                     {
596                         //return "Err:Download failed";
597                     }
598                 }
599                 //スレッドの言語圏のリソース
600                 string curCul2;
601                 if (!Thread.CurrentThread.CurrentCulture.IsNeutralCulture)
602                 {
603                     var idx = Thread.CurrentThread.CurrentCulture.Name.LastIndexOf('-');
604                     if (idx > -1)
605                     {
606                         curCul2 = Thread.CurrentThread.CurrentCulture.Name.Substring(0, idx);
607                     }
608                     else
609                     {
610                         curCul2 = Thread.CurrentThread.CurrentCulture.Name;
611                     }
612                 }
613                 else
614                 {
615                     curCul2 = Thread.CurrentThread.CurrentCulture.Name;
616                 }
617                 if (!string.IsNullOrEmpty(curCul2) && curCul2 != "en" && curCul2 != curCul)
618                 {
619                     if (!Directory.Exists(Path.Combine(MyCommon.settingPath, curCul2)))
620                     {
621                         Directory.CreateDirectory(Path.Combine(MyCommon.settingPath, curCul2));
622                     }
623                     if (!(new HttpVarious()).GetDataToFile("http://tween.sourceforge.jp/TweenRes" + curCul2 + strVer + ".gz?" + DateTime.Now.ToString("yyMMddHHmmss") + Environment.TickCount.ToString(),
624                                                     Path.Combine(Path.Combine(MyCommon.settingPath, curCul2), "Tween.resourcesNew.dll")))
625                     {
626                         //return "Err:Download failed";
627                     }
628                 }
629
630                 //アップデータ
631                 if (!(new HttpVarious()).GetDataToFile("http://tween.sourceforge.jp/TweenUp3.gz?" + DateTime.Now.ToString("yyMMddHHmmss") + Environment.TickCount.ToString(),
632                                                     Path.Combine(MyCommon.settingPath, "TweenUp3.exe")))
633                 {
634                     throw new WebApiException("Err:Download failed");
635                 }
636                 //シリアライザDLL
637                 if (!(new HttpVarious()).GetDataToFile("http://tween.sourceforge.jp/TweenDll" + strVer + ".gz?" + DateTime.Now.ToString("yyMMddHHmmss") + Environment.TickCount.ToString(),
638                                                     Path.Combine(MyCommon.settingPath, "TweenNew.XmlSerializers.dll")))
639                 {
640                     throw new WebApiException("Err:Download failed");
641                 }
642             }
643             catch (Exception ex)
644             {
645                 throw new WebApiException("Err:Download failed", ex);
646             }
647         }
648 #endregion
649
650         public bool ReadOwnPost
651         {
652             get
653             {
654                 return _readOwnPost;
655             }
656             set
657             {
658                 _readOwnPost = value;
659             }
660         }
661
662         public int FollowersCount { get; private set; }
663         public int FriendsCount { get; private set; }
664         public int StatusesCount { get; private set; }
665         public string Location { get; private set; } = "";
666         public string Bio { get; private set; } = "";
667
668         /// <summary>ユーザーのフォロワー数などの情報を更新します</summary>
669         private void UpdateUserStats(TwitterUser self)
670         {
671             this.FollowersCount = self.FollowersCount;
672             this.FriendsCount = self.FriendsCount;
673             this.StatusesCount = self.StatusesCount;
674             this.Location = self.Location;
675             this.Bio = self.Description;
676         }
677
678         /// <summary>
679         /// 渡された取得件数がWORKERTYPEに応じた取得可能範囲に収まっているか検証する
680         /// </summary>
681         public static bool VerifyApiResultCount(MyCommon.WORKERTYPE type, int count)
682         {
683             return count >= 20 && count <= GetMaxApiResultCount(type);
684         }
685
686         /// <summary>
687         /// 渡された取得件数が更新時の取得可能範囲に収まっているか検証する
688         /// </summary>
689         public static bool VerifyMoreApiResultCount(int count)
690         {
691             return count >= 20 && count <= 200;
692         }
693
694         /// <summary>
695         /// 渡された取得件数が起動時の取得可能範囲に収まっているか検証する
696         /// </summary>
697         public static bool VerifyFirstApiResultCount(int count)
698         {
699             return count >= 20 && count <= 200;
700         }
701
702         /// <summary>
703         /// WORKERTYPEに応じた取得可能な最大件数を取得する
704         /// </summary>
705         public static int GetMaxApiResultCount(MyCommon.WORKERTYPE type)
706         {
707             // 参照: REST APIs - 各endpointのcountパラメータ
708             // https://dev.twitter.com/rest/public
709             switch (type)
710             {
711                 case MyCommon.WORKERTYPE.Timeline:
712                 case MyCommon.WORKERTYPE.Reply:
713                 case MyCommon.WORKERTYPE.UserTimeline:
714                 case MyCommon.WORKERTYPE.Favorites:
715                 case MyCommon.WORKERTYPE.DirectMessegeRcv:
716                 case MyCommon.WORKERTYPE.DirectMessegeSnt:
717                 case MyCommon.WORKERTYPE.List:  // 不明
718                     return 200;
719
720                 case MyCommon.WORKERTYPE.PublicSearch:
721                     return 100;
722
723                 default:
724                     throw new InvalidOperationException("Invalid type: " + type);
725             }
726         }
727
728         /// <summary>
729         /// WORKERTYPEに応じた取得件数を取得する
730         /// </summary>
731         public static int GetApiResultCount(MyCommon.WORKERTYPE type, bool more, bool startup)
732         {
733             if (type == MyCommon.WORKERTYPE.DirectMessegeRcv ||
734                 type == MyCommon.WORKERTYPE.DirectMessegeSnt)
735             {
736                 return 20;
737             }
738
739             if (SettingCommon.Instance.UseAdditionalCount)
740             {
741                 switch (type)
742                 {
743                     case MyCommon.WORKERTYPE.Favorites:
744                         if (SettingCommon.Instance.FavoritesCountApi != 0)
745                             return SettingCommon.Instance.FavoritesCountApi;
746                         break;
747                     case MyCommon.WORKERTYPE.List:
748                         if (SettingCommon.Instance.ListCountApi != 0)
749                             return SettingCommon.Instance.ListCountApi;
750                         break;
751                     case MyCommon.WORKERTYPE.PublicSearch:
752                         if (SettingCommon.Instance.SearchCountApi != 0)
753                             return SettingCommon.Instance.SearchCountApi;
754                         break;
755                     case MyCommon.WORKERTYPE.UserTimeline:
756                         if (SettingCommon.Instance.UserTimelineCountApi != 0)
757                             return SettingCommon.Instance.UserTimelineCountApi;
758                         break;
759                 }
760                 if (more && SettingCommon.Instance.MoreCountApi != 0)
761                 {
762                     return Math.Min(SettingCommon.Instance.MoreCountApi, GetMaxApiResultCount(type));
763                 }
764                 if (startup && SettingCommon.Instance.FirstCountApi != 0 && type != MyCommon.WORKERTYPE.Reply)
765                 {
766                     return Math.Min(SettingCommon.Instance.FirstCountApi, GetMaxApiResultCount(type));
767                 }
768             }
769
770             // 上記に当てはまらない場合の共通処理
771             var count = SettingCommon.Instance.CountApi;
772
773             if (type == MyCommon.WORKERTYPE.Reply)
774                 count = SettingCommon.Instance.CountApiReply;
775
776             return Math.Min(count, GetMaxApiResultCount(type));
777         }
778
779         public async Task GetHomeTimelineApi(bool read, HomeTabModel tab, bool more, bool startup)
780         {
781             this.CheckAccountState();
782
783             var count = GetApiResultCount(MyCommon.WORKERTYPE.Timeline, more, startup);
784
785             TwitterStatus[] statuses;
786             if (more)
787             {
788                 statuses = await this.Api.StatusesHomeTimeline(count, maxId: tab.OldestId)
789                     .ConfigureAwait(false);
790             }
791             else
792             {
793                 statuses = await this.Api.StatusesHomeTimeline(count)
794                     .ConfigureAwait(false);
795             }
796
797             var minimumId = this.CreatePostsFromJson(statuses, MyCommon.WORKERTYPE.Timeline, tab, read);
798             if (minimumId != null)
799                 tab.OldestId = minimumId.Value;
800         }
801
802         public async Task GetMentionsTimelineApi(bool read, MentionsTabModel tab, bool more, bool startup)
803         {
804             this.CheckAccountState();
805
806             var count = GetApiResultCount(MyCommon.WORKERTYPE.Reply, more, startup);
807
808             TwitterStatus[] statuses;
809             if (more)
810             {
811                 statuses = await this.Api.StatusesMentionsTimeline(count, maxId: tab.OldestId)
812                     .ConfigureAwait(false);
813             }
814             else
815             {
816                 statuses = await this.Api.StatusesMentionsTimeline(count)
817                     .ConfigureAwait(false);
818             }
819
820             var minimumId = this.CreatePostsFromJson(statuses, MyCommon.WORKERTYPE.Reply, tab, read);
821             if (minimumId != null)
822                 tab.OldestId = minimumId.Value;
823         }
824
825         public async Task GetUserTimelineApi(bool read, string userName, UserTimelineTabModel tab, bool more)
826         {
827             this.CheckAccountState();
828
829             var count = GetApiResultCount(MyCommon.WORKERTYPE.UserTimeline, more, false);
830
831             TwitterStatus[] statuses;
832             if (string.IsNullOrEmpty(userName))
833             {
834                 var target = tab.ScreenName;
835                 if (string.IsNullOrEmpty(target)) return;
836                 userName = target;
837                 statuses = await this.Api.StatusesUserTimeline(userName, count)
838                     .ConfigureAwait(false);
839             }
840             else
841             {
842                 if (more)
843                 {
844                     statuses = await this.Api.StatusesUserTimeline(userName, count, maxId: tab.OldestId)
845                         .ConfigureAwait(false);
846                 }
847                 else
848                 {
849                     statuses = await this.Api.StatusesUserTimeline(userName, count)
850                         .ConfigureAwait(false);
851                 }
852             }
853
854             var minimumId = CreatePostsFromJson(statuses, MyCommon.WORKERTYPE.UserTimeline, tab, read);
855
856             if (minimumId != null)
857                 tab.OldestId = minimumId.Value;
858         }
859
860         public async Task<PostClass> GetStatusApi(bool read, long id)
861         {
862             this.CheckAccountState();
863
864             var status = await this.Api.StatusesShow(id)
865                 .ConfigureAwait(false);
866
867             var item = CreatePostsFromStatusData(status);
868             if (item == null)
869                 throw new WebApiException("Err:Can't create post");
870
871             item.IsRead = read;
872             if (item.IsMe && !read && _readOwnPost) item.IsRead = true;
873
874             return item;
875         }
876
877         public async Task GetStatusApi(bool read, long id, TabModel tab)
878         {
879             var post = await this.GetStatusApi(read, id)
880                 .ConfigureAwait(false);
881
882             //非同期アイコン取得&StatusDictionaryに追加
883             if (tab != null && tab.IsInnerStorageTabType)
884                 tab.AddPostQueue(post);
885             else
886                 TabInformations.GetInstance().AddPost(post);
887         }
888
889         private PostClass CreatePostsFromStatusData(TwitterStatus status)
890         {
891             return CreatePostsFromStatusData(status, false);
892         }
893
894         private PostClass CreatePostsFromStatusData(TwitterStatus status, bool favTweet)
895         {
896             var post = new PostClass();
897             TwitterEntities entities;
898             string sourceHtml;
899
900             post.StatusId = status.Id;
901             if (status.RetweetedStatus != null)
902             {
903                 var retweeted = status.RetweetedStatus;
904
905                 post.CreatedAt = MyCommon.DateTimeParse(retweeted.CreatedAt);
906
907                 //Id
908                 post.RetweetedId = retweeted.Id;
909                 //本文
910                 post.TextFromApi = retweeted.Text;
911                 entities = retweeted.MergedEntities;
912                 sourceHtml = retweeted.Source;
913                 //Reply先
914                 post.InReplyToStatusId = retweeted.InReplyToStatusId;
915                 post.InReplyToUser = retweeted.InReplyToScreenName;
916                 post.InReplyToUserId = status.InReplyToUserId;
917
918                 if (favTweet)
919                 {
920                     post.IsFav = true;
921                 }
922                 else
923                 {
924                     //幻覚fav対策
925                     var tc = TabInformations.GetInstance().GetTabByType(MyCommon.TabUsageType.Favorites);
926                     post.IsFav = tc.Contains(retweeted.Id);
927                 }
928
929                 if (retweeted.Coordinates != null)
930                     post.PostGeo = new PostClass.StatusGeo(retweeted.Coordinates.Coordinates[0], retweeted.Coordinates.Coordinates[1]);
931
932                 //以下、ユーザー情報
933                 var user = retweeted.User;
934
935                 if (user == null || user.ScreenName == null || status.User.ScreenName == null) return null;
936
937                 post.UserId = user.Id;
938                 post.ScreenName = user.ScreenName;
939                 post.Nickname = user.Name.Trim();
940                 post.ImageUrl = user.ProfileImageUrlHttps;
941                 post.IsProtect = user.Protected;
942
943                 //Retweetした人
944                 post.RetweetedBy = status.User.ScreenName;
945                 post.RetweetedByUserId = status.User.Id;
946                 post.IsMe = post.RetweetedBy.ToLowerInvariant().Equals(_uname);
947             }
948             else
949             {
950                 post.CreatedAt = MyCommon.DateTimeParse(status.CreatedAt);
951                 //本文
952                 post.TextFromApi = status.Text;
953                 entities = status.MergedEntities;
954                 sourceHtml = status.Source;
955                 post.InReplyToStatusId = status.InReplyToStatusId;
956                 post.InReplyToUser = status.InReplyToScreenName;
957                 post.InReplyToUserId = status.InReplyToUserId;
958
959                 if (favTweet)
960                 {
961                     post.IsFav = true;
962                 }
963                 else
964                 {
965                     //幻覚fav対策
966                     var tc = TabInformations.GetInstance().GetTabByType(MyCommon.TabUsageType.Favorites);
967                     post.IsFav = tc.Contains(post.StatusId) && TabInformations.GetInstance()[post.StatusId].IsFav;
968                 }
969
970                 if (status.Coordinates != null)
971                     post.PostGeo = new PostClass.StatusGeo(status.Coordinates.Coordinates[0], status.Coordinates.Coordinates[1]);
972
973                 //以下、ユーザー情報
974                 var user = status.User;
975
976                 if (user == null || user.ScreenName == null) return null;
977
978                 post.UserId = user.Id;
979                 post.ScreenName = user.ScreenName;
980                 post.Nickname = user.Name.Trim();
981                 post.ImageUrl = user.ProfileImageUrlHttps;
982                 post.IsProtect = user.Protected;
983                 post.IsMe = post.ScreenName.ToLowerInvariant().Equals(_uname);
984             }
985             //HTMLに整形
986             string textFromApi = post.TextFromApi;
987             post.Text = CreateHtmlAnchor(textFromApi, post.ReplyToList, entities, post.Media);
988             post.TextFromApi = textFromApi;
989             post.TextFromApi = this.ReplaceTextFromApi(post.TextFromApi, entities);
990             post.TextFromApi = WebUtility.HtmlDecode(post.TextFromApi);
991             post.TextFromApi = post.TextFromApi.Replace("<3", "\u2661");
992
993             post.QuoteStatusIds = GetQuoteTweetStatusIds(entities)
994                 .Where(x => x != post.StatusId && x != post.RetweetedId)
995                 .Distinct().ToArray();
996
997             post.ExpandedUrls = entities.OfType<TwitterEntityUrl>()
998                 .Select(x => new PostClass.ExpandedUrlInfo(x.Url, x.ExpandedUrl))
999                 .ToArray();
1000
1001             //Source整形
1002             var source = ParseSource(sourceHtml);
1003             post.Source = source.Item1;
1004             post.SourceUri = source.Item2;
1005
1006             post.IsReply = post.ReplyToList.Contains(_uname);
1007             post.IsExcludeReply = false;
1008
1009             if (post.IsMe)
1010             {
1011                 post.IsOwl = false;
1012             }
1013             else
1014             {
1015                 if (followerId.Count > 0) post.IsOwl = !followerId.Contains(post.UserId);
1016             }
1017
1018             post.IsDm = false;
1019             return post;
1020         }
1021
1022         /// <summary>
1023         /// ツイートに含まれる引用ツイートのURLからステータスIDを抽出
1024         /// </summary>
1025         public static IEnumerable<long> GetQuoteTweetStatusIds(IEnumerable<TwitterEntity> entities)
1026         {
1027             var urls = entities.OfType<TwitterEntityUrl>().Select(x => x.ExpandedUrl);
1028
1029             return GetQuoteTweetStatusIds(urls);
1030         }
1031
1032         public static IEnumerable<long> GetQuoteTweetStatusIds(IEnumerable<string> urls)
1033         {
1034             foreach (var url in urls)
1035             {
1036                 var match = Twitter.StatusUrlRegex.Match(url);
1037                 if (match.Success)
1038                 {
1039                     long statusId;
1040                     if (long.TryParse(match.Groups["StatusId"].Value, out statusId))
1041                         yield return statusId;
1042                 }
1043             }
1044         }
1045
1046         private long? CreatePostsFromJson(TwitterStatus[] items, MyCommon.WORKERTYPE gType, TabModel tab, bool read)
1047         {
1048             long? minimumId = null;
1049
1050             foreach (var status in items)
1051             {
1052                 PostClass post = null;
1053                 post = CreatePostsFromStatusData(status);
1054                 if (post == null) continue;
1055
1056                 if (minimumId == null || minimumId.Value > post.StatusId)
1057                     minimumId = post.StatusId;
1058
1059                 //二重取得回避
1060                 lock (LockObj)
1061                 {
1062                     if (tab == null)
1063                     {
1064                         if (TabInformations.GetInstance().ContainsKey(post.StatusId)) continue;
1065                     }
1066                     else
1067                     {
1068                         if (tab.Contains(post.StatusId)) continue;
1069                     }
1070                 }
1071
1072                 //RT禁止ユーザーによるもの
1073                 if (gType != MyCommon.WORKERTYPE.UserTimeline &&
1074                     post.RetweetedByUserId != null && this.noRTId.Contains(post.RetweetedByUserId.Value)) continue;
1075
1076                 post.IsRead = read;
1077                 if (post.IsMe && !read && _readOwnPost) post.IsRead = true;
1078
1079                 //非同期アイコン取得&StatusDictionaryに追加
1080                 if (tab != null && tab.IsInnerStorageTabType)
1081                     tab.AddPostQueue(post);
1082                 else
1083                     TabInformations.GetInstance().AddPost(post);
1084             }
1085
1086             return minimumId;
1087         }
1088
1089         private long? CreatePostsFromSearchJson(TwitterSearchResult items, TabModel tab, bool read, int count, bool more)
1090         {
1091             long? minimumId = null;
1092
1093             foreach (var result in items.Statuses)
1094             {
1095                 var post = CreatePostsFromStatusData(result);
1096                 if (post == null)
1097                     continue;
1098
1099                 if (minimumId == null || minimumId.Value > post.StatusId)
1100                     minimumId = post.StatusId;
1101
1102                 if (!more && post.StatusId > tab.SinceId) tab.SinceId = post.StatusId;
1103                 //二重取得回避
1104                 lock (LockObj)
1105                 {
1106                     if (tab == null)
1107                     {
1108                         if (TabInformations.GetInstance().ContainsKey(post.StatusId)) continue;
1109                     }
1110                     else
1111                     {
1112                         if (tab.Contains(post.StatusId)) continue;
1113                     }
1114                 }
1115
1116                 post.IsRead = read;
1117                 if ((post.IsMe && !read) && this._readOwnPost) post.IsRead = true;
1118
1119                 //非同期アイコン取得&StatusDictionaryに追加
1120                 if (tab != null && tab.IsInnerStorageTabType)
1121                     tab.AddPostQueue(post);
1122                 else
1123                     TabInformations.GetInstance().AddPost(post);
1124             }
1125
1126             return minimumId;
1127         }
1128
1129         private void CreateFavoritePostsFromJson(TwitterStatus[] item, bool read)
1130         {
1131             var favTab = TabInformations.GetInstance().GetTabByType(MyCommon.TabUsageType.Favorites);
1132
1133             foreach (var status in item)
1134             {
1135                 //二重取得回避
1136                 lock (LockObj)
1137                 {
1138                     if (favTab.Contains(status.Id)) continue;
1139                 }
1140
1141                 var post = CreatePostsFromStatusData(status, true);
1142                 if (post == null) continue;
1143
1144                 post.IsRead = read;
1145
1146                 TabInformations.GetInstance().AddPost(post);
1147             }
1148         }
1149
1150         public async Task GetListStatus(bool read, ListTimelineTabModel tab, bool more, bool startup)
1151         {
1152             var count = GetApiResultCount(MyCommon.WORKERTYPE.List, more, startup);
1153
1154             TwitterStatus[] statuses;
1155             if (more)
1156             {
1157                 statuses = await this.Api.ListsStatuses(tab.ListInfo.Id, count, maxId: tab.OldestId, includeRTs: SettingCommon.Instance.IsListsIncludeRts)
1158                     .ConfigureAwait(false);
1159             }
1160             else
1161             {
1162                 statuses = await this.Api.ListsStatuses(tab.ListInfo.Id, count, includeRTs: SettingCommon.Instance.IsListsIncludeRts)
1163                     .ConfigureAwait(false);
1164             }
1165
1166             var minimumId = CreatePostsFromJson(statuses, MyCommon.WORKERTYPE.List, tab, read);
1167
1168             if (minimumId != null)
1169                 tab.OldestId = minimumId.Value;
1170         }
1171
1172         /// <summary>
1173         /// startStatusId からリプライ先の発言を辿る。発言は posts 以外からは検索しない。
1174         /// </summary>
1175         /// <returns>posts の中から検索されたリプライチェインの末端</returns>
1176         internal static PostClass FindTopOfReplyChain(IDictionary<Int64, PostClass> posts, Int64 startStatusId)
1177         {
1178             if (!posts.ContainsKey(startStatusId))
1179                 throw new ArgumentException("startStatusId (" + startStatusId + ") が posts の中から見つかりませんでした。", nameof(startStatusId));
1180
1181             var nextPost = posts[startStatusId];
1182             while (nextPost.InReplyToStatusId != null)
1183             {
1184                 if (!posts.ContainsKey(nextPost.InReplyToStatusId.Value))
1185                     break;
1186                 nextPost = posts[nextPost.InReplyToStatusId.Value];
1187             }
1188
1189             return nextPost;
1190         }
1191
1192         public async Task GetRelatedResult(bool read, RelatedPostsTabModel tab)
1193         {
1194             var targetPost = tab.TargetPost;
1195             var relPosts = new Dictionary<Int64, PostClass>();
1196             if (targetPost.TextFromApi.Contains("@") && targetPost.InReplyToStatusId == null)
1197             {
1198                 //検索結果対応
1199                 var p = TabInformations.GetInstance()[targetPost.StatusId];
1200                 if (p != null && p.InReplyToStatusId != null)
1201                 {
1202                     targetPost = p;
1203                 }
1204                 else
1205                 {
1206                     p = await this.GetStatusApi(read, targetPost.StatusId)
1207                         .ConfigureAwait(false);
1208                     targetPost = p;
1209                 }
1210             }
1211             relPosts.Add(targetPost.StatusId, targetPost);
1212
1213             Exception lastException = null;
1214
1215             // in_reply_to_status_id を使用してリプライチェインを辿る
1216             var nextPost = FindTopOfReplyChain(relPosts, targetPost.StatusId);
1217             var loopCount = 1;
1218             while (nextPost.InReplyToStatusId != null && loopCount++ <= 20)
1219             {
1220                 var inReplyToId = nextPost.InReplyToStatusId.Value;
1221
1222                 var inReplyToPost = TabInformations.GetInstance()[inReplyToId];
1223                 if (inReplyToPost == null)
1224                 {
1225                     try
1226                     {
1227                         inReplyToPost = await this.GetStatusApi(read, inReplyToId)
1228                             .ConfigureAwait(false);
1229                     }
1230                     catch (WebApiException ex)
1231                     {
1232                         lastException = ex;
1233                         break;
1234                     }
1235                 }
1236
1237                 relPosts.Add(inReplyToPost.StatusId, inReplyToPost);
1238
1239                 nextPost = FindTopOfReplyChain(relPosts, nextPost.StatusId);
1240             }
1241
1242             //MRTとかに対応のためツイート内にあるツイートを指すURLを取り込む
1243             var text = targetPost.Text;
1244             var ma = Twitter.StatusUrlRegex.Matches(text).Cast<Match>()
1245                 .Concat(Twitter.ThirdPartyStatusUrlRegex.Matches(text).Cast<Match>());
1246             foreach (var _match in ma)
1247             {
1248                 Int64 _statusId;
1249                 if (Int64.TryParse(_match.Groups["StatusId"].Value, out _statusId))
1250                 {
1251                     if (relPosts.ContainsKey(_statusId))
1252                         continue;
1253
1254                     var p = TabInformations.GetInstance()[_statusId];
1255                     if (p == null)
1256                     {
1257                         try
1258                         {
1259                             p = await this.GetStatusApi(read, _statusId)
1260                                 .ConfigureAwait(false);
1261                         }
1262                         catch (WebApiException ex)
1263                         {
1264                             lastException = ex;
1265                             break;
1266                         }
1267                     }
1268
1269                     if (p != null)
1270                         relPosts.Add(p.StatusId, p);
1271                 }
1272             }
1273
1274             relPosts.Values.ToList().ForEach(p =>
1275             {
1276                 if (p.IsMe && !read && this._readOwnPost)
1277                     p.IsRead = true;
1278                 else
1279                     p.IsRead = read;
1280
1281                 tab.AddPostQueue(p);
1282             });
1283
1284             if (lastException != null)
1285                 throw new WebApiException(lastException.Message, lastException);
1286         }
1287
1288         public async Task GetSearch(bool read, PublicSearchTabModel tab, bool more)
1289         {
1290             var count = GetApiResultCount(MyCommon.WORKERTYPE.PublicSearch, more, false);
1291
1292             long? maxId = null;
1293             long? sinceId = null;
1294             if (more)
1295             {
1296                 maxId = tab.OldestId - 1;
1297             }
1298             else
1299             {
1300                 sinceId = tab.SinceId;
1301             }
1302
1303             var searchResult = await this.Api.SearchTweets(tab.SearchWords, tab.SearchLang, count, maxId, sinceId)
1304                 .ConfigureAwait(false);
1305
1306             if (!TabInformations.GetInstance().ContainsTab(tab))
1307                 return;
1308
1309             var minimumId = this.CreatePostsFromSearchJson(searchResult, tab, read, count, more);
1310
1311             if (minimumId != null)
1312                 tab.OldestId = minimumId.Value;
1313         }
1314
1315         private void CreateDirectMessagesFromJson(TwitterDirectMessage[] item, MyCommon.WORKERTYPE gType, bool read)
1316         {
1317             foreach (var message in item)
1318             {
1319                 var post = new PostClass();
1320                 try
1321                 {
1322                     post.StatusId = message.Id;
1323                     if (gType != MyCommon.WORKERTYPE.UserStream)
1324                     {
1325                         if (gType == MyCommon.WORKERTYPE.DirectMessegeRcv)
1326                         {
1327                             if (minDirectmessage > post.StatusId) minDirectmessage = post.StatusId;
1328                         }
1329                         else
1330                         {
1331                             if (minDirectmessageSent > post.StatusId) minDirectmessageSent = post.StatusId;
1332                         }
1333                     }
1334
1335                     //二重取得回避
1336                     lock (LockObj)
1337                     {
1338                         if (TabInformations.GetInstance().GetTabByType(MyCommon.TabUsageType.DirectMessage).Contains(post.StatusId)) continue;
1339                     }
1340                     //sender_id
1341                     //recipient_id
1342                     post.CreatedAt = MyCommon.DateTimeParse(message.CreatedAt);
1343                     //本文
1344                     var textFromApi = message.Text;
1345                     //HTMLに整形
1346                     post.Text = CreateHtmlAnchor(textFromApi, post.ReplyToList, message.Entities, post.Media);
1347                     post.TextFromApi = this.ReplaceTextFromApi(textFromApi, message.Entities);
1348                     post.TextFromApi = WebUtility.HtmlDecode(post.TextFromApi);
1349                     post.TextFromApi = post.TextFromApi.Replace("<3", "\u2661");
1350                     post.IsFav = false;
1351
1352                     post.QuoteStatusIds = GetQuoteTweetStatusIds(message.Entities).Distinct().ToArray();
1353
1354                     post.ExpandedUrls = message.Entities.OfType<TwitterEntityUrl>()
1355                         .Select(x => new PostClass.ExpandedUrlInfo(x.Url, x.ExpandedUrl))
1356                         .ToArray();
1357
1358                     //以下、ユーザー情報
1359                     TwitterUser user;
1360                     if (gType == MyCommon.WORKERTYPE.UserStream)
1361                     {
1362                         if (this.Api.CurrentUserId == message.Recipient.Id)
1363                         {
1364                             user = message.Sender;
1365                             post.IsMe = false;
1366                             post.IsOwl = true;
1367                         }
1368                         else
1369                         {
1370                             user = message.Recipient;
1371                             post.IsMe = true;
1372                             post.IsOwl = false;
1373                         }
1374                     }
1375                     else
1376                     {
1377                         if (gType == MyCommon.WORKERTYPE.DirectMessegeRcv)
1378                         {
1379                             user = message.Sender;
1380                             post.IsMe = false;
1381                             post.IsOwl = true;
1382                         }
1383                         else
1384                         {
1385                             user = message.Recipient;
1386                             post.IsMe = true;
1387                             post.IsOwl = false;
1388                         }
1389                     }
1390
1391                     post.UserId = user.Id;
1392                     post.ScreenName = user.ScreenName;
1393                     post.Nickname = user.Name.Trim();
1394                     post.ImageUrl = user.ProfileImageUrlHttps;
1395                     post.IsProtect = user.Protected;
1396                 }
1397                 catch(Exception ex)
1398                 {
1399                     MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name);
1400                     MessageBox.Show("Parse Error(CreateDirectMessagesFromJson)");
1401                     continue;
1402                 }
1403
1404                 post.IsRead = read;
1405                 if (post.IsMe && !read && _readOwnPost) post.IsRead = true;
1406                 post.IsReply = false;
1407                 post.IsExcludeReply = false;
1408                 post.IsDm = true;
1409
1410                 var dmTab = TabInformations.GetInstance().GetTabByType(MyCommon.TabUsageType.DirectMessage);
1411                 dmTab.AddPostQueue(post);
1412             }
1413         }
1414
1415         public async Task GetDirectMessageApi(bool read, MyCommon.WORKERTYPE gType, bool more)
1416         {
1417             this.CheckAccountState();
1418             this.CheckAccessLevel(TwitterApiAccessLevel.ReadWriteAndDirectMessage);
1419
1420             var count = GetApiResultCount(gType, more, false);
1421
1422             TwitterDirectMessage[] messages;
1423             if (gType == MyCommon.WORKERTYPE.DirectMessegeRcv)
1424             {
1425                 if (more)
1426                 {
1427                     messages = await this.Api.DirectMessagesRecv(count, maxId: this.minDirectmessage)
1428                         .ConfigureAwait(false);
1429                 }
1430                 else
1431                 {
1432                     messages = await this.Api.DirectMessagesRecv(count)
1433                         .ConfigureAwait(false);
1434                 }
1435             }
1436             else
1437             {
1438                 if (more)
1439                 {
1440                     messages = await this.Api.DirectMessagesSent(count, maxId: this.minDirectmessageSent)
1441                         .ConfigureAwait(false);
1442                 }
1443                 else
1444                 {
1445                     messages = await this.Api.DirectMessagesSent(count)
1446                         .ConfigureAwait(false);
1447                 }
1448             }
1449
1450             CreateDirectMessagesFromJson(messages, gType, read);
1451         }
1452
1453         public async Task GetFavoritesApi(bool read, bool more)
1454         {
1455             this.CheckAccountState();
1456
1457             var count = GetApiResultCount(MyCommon.WORKERTYPE.Favorites, more, false);
1458
1459             var statuses = await this.Api.FavoritesList(count)
1460                 .ConfigureAwait(false);
1461
1462             CreateFavoritePostsFromJson(statuses, read);
1463         }
1464
1465         private string ReplaceTextFromApi(string text, TwitterEntities entities)
1466         {
1467             if (entities != null)
1468             {
1469                 if (entities.Urls != null)
1470                 {
1471                     foreach (var m in entities.Urls)
1472                     {
1473                         if (!string.IsNullOrEmpty(m.DisplayUrl)) text = text.Replace(m.Url, m.DisplayUrl);
1474                     }
1475                 }
1476                 if (entities.Media != null)
1477                 {
1478                     foreach (var m in entities.Media)
1479                     {
1480                         if (m.AltText != null)
1481                         {
1482                             text = text.Replace(m.Url, string.Format(Properties.Resources.ImageAltText, m.AltText));
1483                         }
1484                         else
1485                         {
1486                             if (!string.IsNullOrEmpty(m.DisplayUrl)) text = text.Replace(m.Url, m.DisplayUrl);
1487                         }
1488                     }
1489                 }
1490             }
1491             return text;
1492         }
1493
1494         /// <summary>
1495         /// フォロワーIDを更新します
1496         /// </summary>
1497         /// <exception cref="WebApiException"/>
1498         public async Task RefreshFollowerIds()
1499         {
1500             if (MyCommon._endingFlag) return;
1501
1502             var cursor = -1L;
1503             var newFollowerIds = new HashSet<long>();
1504             do
1505             {
1506                 var ret = await this.Api.FollowersIds(cursor)
1507                     .ConfigureAwait(false);
1508
1509                 if (ret.Ids == null)
1510                     throw new WebApiException("ret.ids == null");
1511
1512                 newFollowerIds.UnionWith(ret.Ids);
1513                 cursor = ret.NextCursor;
1514             } while (cursor != 0);
1515
1516             this.followerId = newFollowerIds;
1517             TabInformations.GetInstance().RefreshOwl(this.followerId);
1518
1519             this._GetFollowerResult = true;
1520         }
1521
1522         public bool GetFollowersSuccess
1523         {
1524             get
1525             {
1526                 return _GetFollowerResult;
1527             }
1528         }
1529
1530         /// <summary>
1531         /// RT 非表示ユーザーを更新します
1532         /// </summary>
1533         /// <exception cref="WebApiException"/>
1534         public async Task RefreshNoRetweetIds()
1535         {
1536             if (MyCommon._endingFlag) return;
1537
1538             this.noRTId = await this.Api.NoRetweetIds()
1539                 .ConfigureAwait(false);
1540
1541             this._GetNoRetweetResult = true;
1542         }
1543
1544         public bool GetNoRetweetSuccess
1545         {
1546             get
1547             {
1548                 return _GetNoRetweetResult;
1549             }
1550         }
1551
1552         /// <summary>
1553         /// t.co の文字列長などの設定情報を更新します
1554         /// </summary>
1555         /// <exception cref="WebApiException"/>
1556         public async Task RefreshConfiguration()
1557         {
1558             this.Configuration = await this.Api.Configuration()
1559                 .ConfigureAwait(false);
1560         }
1561
1562         public async Task GetListsApi()
1563         {
1564             this.CheckAccountState();
1565
1566             var ownedLists = await TwitterLists.GetAllItemsAsync(x => this.Api.ListsOwnerships(this.Username, cursor: x))
1567                 .ConfigureAwait(false);
1568
1569             var subscribedLists = await TwitterLists.GetAllItemsAsync(x => this.Api.ListsSubscriptions(this.Username, cursor: x))
1570                 .ConfigureAwait(false);
1571
1572             TabInformations.GetInstance().SubscribableLists = Enumerable.Concat(ownedLists, subscribedLists)
1573                 .Select(x => new ListElement(x, this))
1574                 .ToList();
1575         }
1576
1577         public async Task DeleteList(long listId)
1578         {
1579             await this.Api.ListsDestroy(listId)
1580                 .IgnoreResponse()
1581                 .ConfigureAwait(false);
1582
1583             var tabinfo = TabInformations.GetInstance();
1584
1585             tabinfo.SubscribableLists = tabinfo.SubscribableLists
1586                 .Where(x => x.Id != listId)
1587                 .ToList();
1588         }
1589
1590         public async Task<ListElement> EditList(long listId, string new_name, bool isPrivate, string description)
1591         {
1592             var response = await this.Api.ListsUpdate(listId, new_name, description, isPrivate)
1593                 .ConfigureAwait(false);
1594
1595             var list = await response.LoadJsonAsync()
1596                 .ConfigureAwait(false);
1597
1598             return new ListElement(list, this);
1599         }
1600
1601         public async Task<long> GetListMembers(long listId, List<UserInfo> lists, long cursor)
1602         {
1603             this.CheckAccountState();
1604
1605             var users = await this.Api.ListsMembers(listId, cursor)
1606                 .ConfigureAwait(false);
1607
1608             Array.ForEach(users.Users, u => lists.Add(new UserInfo(u)));
1609
1610             return users.NextCursor;
1611         }
1612
1613         public async Task CreateListApi(string listName, bool isPrivate, string description)
1614         {
1615             this.CheckAccountState();
1616
1617             var response = await this.Api.ListsCreate(listName, description, isPrivate)
1618                 .ConfigureAwait(false);
1619
1620             var list = await response.LoadJsonAsync()
1621                 .ConfigureAwait(false);
1622
1623             TabInformations.GetInstance().SubscribableLists.Add(new ListElement(list, this));
1624         }
1625
1626         public async Task<bool> ContainsUserAtList(long listId, string user)
1627         {
1628             this.CheckAccountState();
1629
1630             try
1631             {
1632                 await this.Api.ListsMembersShow(listId, user)
1633                     .ConfigureAwait(false);
1634
1635                 return true;
1636             }
1637             catch (TwitterApiException ex)
1638                 when (ex.ErrorResponse.Errors.Any(x => x.Code == TwitterErrorCode.NotFound))
1639             {
1640                 return false;
1641             }
1642         }
1643
1644         public string CreateHtmlAnchor(string text, List<string> AtList, TwitterEntities entities, List<MediaInfo> media)
1645         {
1646             if (entities != null)
1647             {
1648                 if (entities.Hashtags != null)
1649                 {
1650                     lock (this.LockObj)
1651                     {
1652                         this._hashList.AddRange(entities.Hashtags.Select(x => "#" + x.Text));
1653                     }
1654                 }
1655                 if (entities.UserMentions != null)
1656                 {
1657                     foreach (var ent in entities.UserMentions)
1658                     {
1659                         var screenName = ent.ScreenName.ToLowerInvariant();
1660                         if (!AtList.Contains(screenName))
1661                             AtList.Add(screenName);
1662                     }
1663                 }
1664                 if (entities.Media != null)
1665                 {
1666                     if (media != null)
1667                     {
1668                         foreach (var ent in entities.Media)
1669                         {
1670                             if (!media.Any(x => x.Url == ent.MediaUrl))
1671                             {
1672                                 if (ent.VideoInfo != null &&
1673                                     ent.Type == "animated_gif" || ent.Type == "video")
1674                                 {
1675                                     //var videoUrl = ent.VideoInfo.Variants
1676                                     //    .Where(v => v.ContentType == "video/mp4")
1677                                     //    .OrderByDescending(v => v.Bitrate)
1678                                     //    .Select(v => v.Url).FirstOrDefault();
1679                                     media.Add(new MediaInfo(ent.MediaUrl, ent.AltText, ent.ExpandedUrl));
1680                                 }
1681                                 else
1682                                     media.Add(new MediaInfo(ent.MediaUrl, ent.AltText, videoUrl: null));
1683                             }
1684                         }
1685                     }
1686                 }
1687             }
1688
1689             // PostClass.ExpandedUrlInfo を使用して非同期に URL 展開を行うためここでは expanded_url を使用しない
1690             text = TweetFormatter.AutoLinkHtml(text, entities, keepTco: true);
1691
1692             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>");
1693             text = PreProcessUrl(text); //IDN置換
1694
1695             return text;
1696         }
1697
1698         private static readonly Uri SourceUriBase = new Uri("https://twitter.com/");
1699
1700         /// <summary>
1701         /// Twitter APIから得たHTML形式のsource文字列を分析し、source名とURLに分離します
1702         /// </summary>
1703         public static Tuple<string, Uri> ParseSource(string sourceHtml)
1704         {
1705             if (string.IsNullOrEmpty(sourceHtml))
1706                 return Tuple.Create<string, Uri>("", null);
1707
1708             string sourceText;
1709             Uri sourceUri;
1710
1711             // sourceHtmlの例: <a href="http://twitter.com" rel="nofollow">Twitter Web Client</a>
1712
1713             var match = Regex.Match(sourceHtml, "^<a href=\"(?<uri>.+?)\".*?>(?<text>.+)</a>$", RegexOptions.IgnoreCase);
1714             if (match.Success)
1715             {
1716                 sourceText = WebUtility.HtmlDecode(match.Groups["text"].Value);
1717                 try
1718                 {
1719                     var uriStr = WebUtility.HtmlDecode(match.Groups["uri"].Value);
1720                     sourceUri = new Uri(SourceUriBase, uriStr);
1721                 }
1722                 catch (UriFormatException)
1723                 {
1724                     sourceUri = null;
1725                 }
1726             }
1727             else
1728             {
1729                 sourceText = WebUtility.HtmlDecode(sourceHtml);
1730                 sourceUri = null;
1731             }
1732
1733             return Tuple.Create(sourceText, sourceUri);
1734         }
1735
1736         public async Task<TwitterApiStatus> GetInfoApi()
1737         {
1738             if (Twitter.AccountState != MyCommon.ACCOUNT_STATE.Valid) return null;
1739
1740             if (MyCommon._endingFlag) return null;
1741
1742             var limits = await this.Api.ApplicationRateLimitStatus()
1743                 .ConfigureAwait(false);
1744
1745             MyCommon.TwitterApiInfo.UpdateFromJson(limits);
1746
1747             return MyCommon.TwitterApiInfo;
1748         }
1749
1750         /// <summary>
1751         /// ブロック中のユーザーを更新します
1752         /// </summary>
1753         /// <exception cref="WebApiException"/>
1754         public async Task RefreshBlockIds()
1755         {
1756             if (MyCommon._endingFlag) return;
1757
1758             var cursor = -1L;
1759             var newBlockIds = new HashSet<long>();
1760             do
1761             {
1762                 var ret = await this.Api.BlocksIds(cursor)
1763                     .ConfigureAwait(false);
1764
1765                 newBlockIds.UnionWith(ret.Ids);
1766                 cursor = ret.NextCursor;
1767             } while (cursor != 0);
1768
1769             newBlockIds.Remove(this.UserId); // 元のソースにあったので一応残しておく
1770
1771             TabInformations.GetInstance().BlockIds = newBlockIds;
1772         }
1773
1774         /// <summary>
1775         /// ミュート中のユーザーIDを更新します
1776         /// </summary>
1777         /// <exception cref="WebApiException"/>
1778         public async Task RefreshMuteUserIdsAsync()
1779         {
1780             if (MyCommon._endingFlag) return;
1781
1782             var ids = await TwitterIds.GetAllItemsAsync(x => this.Api.MutesUsersIds(x))
1783                 .ConfigureAwait(false);
1784
1785             TabInformations.GetInstance().MuteUserIds = new HashSet<long>(ids);
1786         }
1787
1788         public string[] GetHashList()
1789         {
1790             string[] hashArray;
1791             lock (LockObj)
1792             {
1793                 hashArray = _hashList.ToArray();
1794                 _hashList.Clear();
1795             }
1796             return hashArray;
1797         }
1798
1799         public string AccessToken
1800             => ((TwitterApiConnection)this.Api.Connection).AccessToken;
1801
1802         public string AccessTokenSecret
1803             => ((TwitterApiConnection)this.Api.Connection).AccessSecret;
1804
1805         private void CheckAccountState()
1806         {
1807             if (Twitter.AccountState != MyCommon.ACCOUNT_STATE.Valid)
1808                 throw new WebApiException("Auth error. Check your account");
1809         }
1810
1811         private void CheckAccessLevel(TwitterApiAccessLevel accessLevelFlags)
1812         {
1813             if (!this.AccessLevel.HasFlag(accessLevelFlags))
1814                 throw new WebApiException("Auth Err:try to re-authorization.");
1815         }
1816
1817         private void CheckStatusCode(HttpStatusCode httpStatus, string responseText,
1818             [CallerMemberName] string callerMethodName = "")
1819         {
1820             if (httpStatus == HttpStatusCode.OK)
1821             {
1822                 Twitter.AccountState = MyCommon.ACCOUNT_STATE.Valid;
1823                 return;
1824             }
1825
1826             if (string.IsNullOrWhiteSpace(responseText))
1827             {
1828                 if (httpStatus == HttpStatusCode.Unauthorized)
1829                     Twitter.AccountState = MyCommon.ACCOUNT_STATE.Invalid;
1830
1831                 throw new WebApiException("Err:" + httpStatus + "(" + callerMethodName + ")");
1832             }
1833
1834             try
1835             {
1836                 var errors = TwitterError.ParseJson(responseText).Errors;
1837                 if (errors == null || !errors.Any())
1838                 {
1839                     throw new WebApiException("Err:" + httpStatus + "(" + callerMethodName + ")", responseText);
1840                 }
1841
1842                 foreach (var error in errors)
1843                 {
1844                     if (error.Code == TwitterErrorCode.InvalidToken ||
1845                         error.Code == TwitterErrorCode.SuspendedAccount)
1846                     {
1847                         Twitter.AccountState = MyCommon.ACCOUNT_STATE.Invalid;
1848                     }
1849                 }
1850
1851                 throw new WebApiException("Err:" + string.Join(",", errors.Select(x => x.ToString())) + "(" + callerMethodName + ")", responseText);
1852             }
1853             catch (SerializationException) { }
1854
1855             throw new WebApiException("Err:" + httpStatus + "(" + callerMethodName + ")", responseText);
1856         }
1857
1858         public int GetTextLengthRemain(string postText)
1859         {
1860             var matchDm = Twitter.DMSendTextRegex.Match(postText);
1861             if (matchDm.Success)
1862                 return this.GetTextLengthRemainInternal(matchDm.Groups["body"].Value, isDm: true);
1863
1864             return this.GetTextLengthRemainInternal(postText, isDm: false);
1865         }
1866
1867         private int GetTextLengthRemainInternal(string postText, bool isDm)
1868         {
1869             var textLength = 0;
1870
1871             var pos = 0;
1872             while (pos < postText.Length)
1873             {
1874                 textLength++;
1875
1876                 if (char.IsSurrogatePair(postText, pos))
1877                     pos += 2; // サロゲートペアの場合は2文字分進める
1878                 else
1879                     pos++;
1880             }
1881
1882             var urls = TweetExtractor.ExtractUrls(postText);
1883             foreach (var url in urls)
1884             {
1885                 var shortUrlLength = url.StartsWith("https://", StringComparison.OrdinalIgnoreCase)
1886                     ? this.Configuration.ShortUrlLengthHttps
1887                     : this.Configuration.ShortUrlLength;
1888
1889                 textLength += shortUrlLength - url.Length;
1890             }
1891
1892             if (isDm)
1893                 return this.Configuration.DmTextCharacterLimit - textLength;
1894             else
1895                 return 140 - textLength;
1896         }
1897
1898
1899 #region "UserStream"
1900         private string trackWord_ = "";
1901         public string TrackWord
1902         {
1903             get
1904             {
1905                 return trackWord_;
1906             }
1907             set
1908             {
1909                 trackWord_ = value;
1910             }
1911         }
1912         private bool allAtReply_ = false;
1913         public bool AllAtReply
1914         {
1915             get
1916             {
1917                 return allAtReply_;
1918             }
1919             set
1920             {
1921                 allAtReply_ = value;
1922             }
1923         }
1924
1925         public event EventHandler NewPostFromStream;
1926         public event EventHandler UserStreamStarted;
1927         public event EventHandler UserStreamStopped;
1928         public event EventHandler<PostDeletedEventArgs> PostDeleted;
1929         public event EventHandler<UserStreamEventReceivedEventArgs> UserStreamEventReceived;
1930         private DateTime _lastUserstreamDataReceived;
1931         private TwitterUserstream userStream;
1932
1933         public class FormattedEvent
1934         {
1935             public MyCommon.EVENTTYPE Eventtype { get; set; }
1936             public DateTime CreatedAt { get; set; }
1937             public string Event { get; set; }
1938             public string Username { get; set; }
1939             public string Target { get; set; }
1940             public Int64 Id { get; set; }
1941             public bool IsMe { get; set; }
1942         }
1943
1944         public List<FormattedEvent> storedEvent_ = new List<FormattedEvent>();
1945         public List<FormattedEvent> StoredEvent
1946         {
1947             get
1948             {
1949                 return storedEvent_;
1950             }
1951             set
1952             {
1953                 storedEvent_ = value;
1954             }
1955         }
1956
1957         private readonly IReadOnlyDictionary<string, MyCommon.EVENTTYPE> eventTable = new Dictionary<string, MyCommon.EVENTTYPE>
1958         {
1959             ["favorite"] = MyCommon.EVENTTYPE.Favorite,
1960             ["unfavorite"] = MyCommon.EVENTTYPE.Unfavorite,
1961             ["follow"] = MyCommon.EVENTTYPE.Follow,
1962             ["list_member_added"] = MyCommon.EVENTTYPE.ListMemberAdded,
1963             ["list_member_removed"] = MyCommon.EVENTTYPE.ListMemberRemoved,
1964             ["block"] = MyCommon.EVENTTYPE.Block,
1965             ["unblock"] = MyCommon.EVENTTYPE.Unblock,
1966             ["user_update"] = MyCommon.EVENTTYPE.UserUpdate,
1967             ["deleted"] = MyCommon.EVENTTYPE.Deleted,
1968             ["list_created"] = MyCommon.EVENTTYPE.ListCreated,
1969             ["list_destroyed"] = MyCommon.EVENTTYPE.ListDestroyed,
1970             ["list_updated"] = MyCommon.EVENTTYPE.ListUpdated,
1971             ["unfollow"] = MyCommon.EVENTTYPE.Unfollow,
1972             ["list_user_subscribed"] = MyCommon.EVENTTYPE.ListUserSubscribed,
1973             ["list_user_unsubscribed"] = MyCommon.EVENTTYPE.ListUserUnsubscribed,
1974             ["mute"] = MyCommon.EVENTTYPE.Mute,
1975             ["unmute"] = MyCommon.EVENTTYPE.Unmute,
1976             ["quoted_tweet"] = MyCommon.EVENTTYPE.QuotedTweet,
1977         };
1978
1979         public bool IsUserstreamDataReceived
1980         {
1981             get
1982             {
1983                 return DateTime.Now.Subtract(this._lastUserstreamDataReceived).TotalSeconds < 31;
1984             }
1985         }
1986
1987         private void userStream_StatusArrived(string line)
1988         {
1989             this._lastUserstreamDataReceived = DateTime.Now;
1990             if (string.IsNullOrEmpty(line)) return;
1991
1992             if (line.First() != '{' || line.Last() != '}')
1993             {
1994                 MyCommon.TraceOut("Invalid JSON (StatusArrived):" + Environment.NewLine + line);
1995                 return;
1996             }
1997
1998             var isDm = false;
1999
2000             try
2001             {
2002                 using (var jsonReader = JsonReaderWriterFactory.CreateJsonReader(Encoding.UTF8.GetBytes(line), XmlDictionaryReaderQuotas.Max))
2003                 {
2004                     var xElm = XElement.Load(jsonReader);
2005                     if (xElm.Element("friends") != null)
2006                     {
2007                         Debug.WriteLine("friends");
2008                         return;
2009                     }
2010                     else if (xElm.Element("delete") != null)
2011                     {
2012                         Debug.WriteLine("delete");
2013                         Int64 id;
2014                         XElement idElm;
2015                         if ((idElm = xElm.Element("delete").Element("direct_message")?.Element("id")) != null)
2016                         {
2017                             id = 0;
2018                             long.TryParse(idElm.Value, out id);
2019
2020                             this.PostDeleted?.Invoke(this, new PostDeletedEventArgs(id));
2021                         }
2022                         else if ((idElm = xElm.Element("delete").Element("status")?.Element("id")) != null)
2023                         {
2024                             id = 0;
2025                             long.TryParse(idElm.Value, out id);
2026
2027                             this.PostDeleted?.Invoke(this, new PostDeletedEventArgs(id));
2028                         }
2029                         else
2030                         {
2031                             MyCommon.TraceOut("delete:" + line);
2032                             return;
2033                         }
2034                         for (int i = this.StoredEvent.Count - 1; i >= 0; i--)
2035                         {
2036                             var sEvt = this.StoredEvent[i];
2037                             if (sEvt.Id == id && (sEvt.Event == "favorite" || sEvt.Event == "unfavorite"))
2038                             {
2039                                 this.StoredEvent.RemoveAt(i);
2040                             }
2041                         }
2042                         return;
2043                     }
2044                     else if (xElm.Element("limit") != null)
2045                     {
2046                         Debug.WriteLine(line);
2047                         return;
2048                     }
2049                     else if (xElm.Element("event") != null)
2050                     {
2051                         Debug.WriteLine("event: " + xElm.Element("event").Value);
2052                         CreateEventFromJson(line);
2053                         return;
2054                     }
2055                     else if (xElm.Element("direct_message") != null)
2056                     {
2057                         Debug.WriteLine("direct_message");
2058                         isDm = true;
2059                     }
2060                     else if (xElm.Element("retweeted_status") != null)
2061                     {
2062                         var sourceUserId = xElm.XPathSelectElement("/user/id_str").Value;
2063                         var targetUserId = xElm.XPathSelectElement("/retweeted_status/user/id_str").Value;
2064
2065                         // 自分に関係しないリツイートの場合は無視する
2066                         var selfUserId = this.UserId.ToString();
2067                         if (sourceUserId == selfUserId || targetUserId == selfUserId)
2068                         {
2069                             // 公式 RT をイベントとしても扱う
2070                             var evt = CreateEventFromRetweet(xElm);
2071                             if (evt != null)
2072                             {
2073                                 this.StoredEvent.Insert(0, evt);
2074
2075                                 this.UserStreamEventReceived?.Invoke(this, new UserStreamEventReceivedEventArgs(evt));
2076                             }
2077                         }
2078
2079                         // 従来通り公式 RT の表示も行うため return しない
2080                     }
2081                     else if (xElm.Element("scrub_geo") != null)
2082                     {
2083                         try
2084                         {
2085                             TabInformations.GetInstance().ScrubGeoReserve(long.Parse(xElm.Element("scrub_geo").Element("user_id").Value),
2086                                                                         long.Parse(xElm.Element("scrub_geo").Element("up_to_status_id").Value));
2087                         }
2088                         catch(Exception)
2089                         {
2090                             MyCommon.TraceOut("scrub_geo:" + line);
2091                         }
2092                         return;
2093                     }
2094                 }
2095
2096                 if (isDm)
2097                 {
2098                     try
2099                     {
2100                         var message = TwitterStreamEventDirectMessage.ParseJson(line).DirectMessage;
2101                         this.CreateDirectMessagesFromJson(new[] { message }, MyCommon.WORKERTYPE.UserStream, false);
2102                     }
2103                     catch (SerializationException ex)
2104                     {
2105                         throw TwitterApiException.CreateFromException(ex, line);
2106                     }
2107                 }
2108                 else
2109                 {
2110                     try
2111                     {
2112                         var status = TwitterStatus.ParseJson(line);
2113                         this.CreatePostsFromJson(new[] { status }, MyCommon.WORKERTYPE.UserStream, null, false);
2114                     }
2115                     catch (SerializationException ex)
2116                     {
2117                         throw TwitterApiException.CreateFromException(ex, line);
2118                     }
2119                 }
2120             }
2121             catch (WebApiException ex)
2122             {
2123                 MyCommon.TraceOut(ex);
2124                 return;
2125             }
2126             catch(NullReferenceException)
2127             {
2128                 MyCommon.TraceOut("NullRef StatusArrived: " + line);
2129             }
2130
2131             this.NewPostFromStream?.Invoke(this, EventArgs.Empty);
2132         }
2133
2134         /// <summary>
2135         /// UserStreamsから受信した公式RTをイベントに変換します
2136         /// </summary>
2137         private FormattedEvent CreateEventFromRetweet(XElement xElm)
2138         {
2139             return new FormattedEvent
2140             {
2141                 Eventtype = MyCommon.EVENTTYPE.Retweet,
2142                 Event = "retweet",
2143                 CreatedAt = MyCommon.DateTimeParse(xElm.XPathSelectElement("/created_at").Value),
2144                 IsMe = xElm.XPathSelectElement("/user/id_str").Value == this.UserId.ToString(),
2145                 Username = xElm.XPathSelectElement("/user/screen_name").Value,
2146                 Target = string.Format("@{0}:{1}", new[]
2147                 {
2148                     xElm.XPathSelectElement("/retweeted_status/user/screen_name").Value,
2149                     WebUtility.HtmlDecode(xElm.XPathSelectElement("/retweeted_status/text").Value),
2150                 }),
2151                 Id = long.Parse(xElm.XPathSelectElement("/retweeted_status/id_str").Value),
2152             };
2153         }
2154
2155         private void CreateEventFromJson(string content)
2156         {
2157             TwitterStreamEvent eventData = null;
2158             try
2159             {
2160                 eventData = TwitterStreamEvent.ParseJson(content);
2161             }
2162             catch(SerializationException ex)
2163             {
2164                 MyCommon.TraceOut(ex, "Event Serialize Exception!" + Environment.NewLine + content);
2165             }
2166             catch(Exception ex)
2167             {
2168                 MyCommon.TraceOut(ex, "Event Exception!" + Environment.NewLine + content);
2169             }
2170
2171             var evt = new FormattedEvent();
2172             evt.CreatedAt = MyCommon.DateTimeParse(eventData.CreatedAt);
2173             evt.Event = eventData.Event;
2174             evt.Username = eventData.Source.ScreenName;
2175             evt.IsMe = evt.Username.ToLowerInvariant().Equals(this.Username.ToLowerInvariant());
2176
2177             MyCommon.EVENTTYPE eventType;
2178             eventTable.TryGetValue(eventData.Event, out eventType);
2179             evt.Eventtype = eventType;
2180
2181             TwitterStreamEvent<TwitterStatus> tweetEvent;
2182
2183             switch (eventData.Event)
2184             {
2185                 case "access_revoked":
2186                 case "access_unrevoked":
2187                 case "user_delete":
2188                 case "user_suspend":
2189                     return;
2190                 case "follow":
2191                     if (eventData.Target.ScreenName.ToLowerInvariant().Equals(_uname))
2192                     {
2193                         if (!this.followerId.Contains(eventData.Source.Id)) this.followerId.Add(eventData.Source.Id);
2194                     }
2195                     else
2196                     {
2197                         return;    //Block後のUndoをすると、SourceとTargetが逆転したfollowイベントが帰ってくるため。
2198                     }
2199                     evt.Target = "";
2200                     break;
2201                 case "unfollow":
2202                     evt.Target = "@" + eventData.Target.ScreenName;
2203                     break;
2204                 case "favorited_retweet":
2205                 case "retweeted_retweet":
2206                     return;
2207                 case "favorite":
2208                 case "unfavorite":
2209                     tweetEvent = TwitterStreamEvent<TwitterStatus>.ParseJson(content);
2210                     evt.Target = "@" + tweetEvent.TargetObject.User.ScreenName + ":" + WebUtility.HtmlDecode(tweetEvent.TargetObject.Text);
2211                     evt.Id = tweetEvent.TargetObject.Id;
2212
2213                     if (SettingCommon.Instance.IsRemoveSameEvent)
2214                     {
2215                         if (this.StoredEvent.Any(ev => ev.Username == evt.Username && ev.Eventtype == evt.Eventtype && ev.Target == evt.Target))
2216                             return;
2217                     }
2218
2219                     var tabinfo = TabInformations.GetInstance();
2220
2221                     PostClass post;
2222                     var statusId = tweetEvent.TargetObject.Id;
2223                     if (!tabinfo.Posts.TryGetValue(statusId, out post))
2224                         break;
2225
2226                     if (eventData.Event == "favorite")
2227                     {
2228                         var favTab = tabinfo.GetTabByType(MyCommon.TabUsageType.Favorites);
2229                         if (!favTab.Contains(post.StatusId))
2230                             favTab.AddPostImmediately(post.StatusId, post.IsRead);
2231
2232                         if (tweetEvent.Source.Id == this.UserId)
2233                         {
2234                             post.IsFav = true;
2235                         }
2236                         else if (tweetEvent.Target.Id == this.UserId)
2237                         {
2238                             post.FavoritedCount++;
2239
2240                             if (SettingCommon.Instance.FavEventUnread)
2241                                 tabinfo.SetReadAllTab(post.StatusId, read: false);
2242                         }
2243                     }
2244                     else // unfavorite
2245                     {
2246                         if (tweetEvent.Source.Id == this.UserId)
2247                         {
2248                             post.IsFav = false;
2249                         }
2250                         else if (tweetEvent.Target.Id == this.UserId)
2251                         {
2252                             post.FavoritedCount = Math.Max(0, post.FavoritedCount - 1);
2253                         }
2254                     }
2255                     break;
2256                 case "quoted_tweet":
2257                     if (evt.IsMe) return;
2258
2259                     tweetEvent = TwitterStreamEvent<TwitterStatus>.ParseJson(content);
2260                     evt.Target = "@" + tweetEvent.TargetObject.User.ScreenName + ":" + WebUtility.HtmlDecode(tweetEvent.TargetObject.Text);
2261                     evt.Id = tweetEvent.TargetObject.Id;
2262
2263                     if (SettingCommon.Instance.IsRemoveSameEvent)
2264                     {
2265                         if (this.StoredEvent.Any(ev => ev.Username == evt.Username && ev.Eventtype == evt.Eventtype && ev.Target == evt.Target))
2266                             return;
2267                     }
2268                     break;
2269                 case "list_member_added":
2270                 case "list_member_removed":
2271                 case "list_created":
2272                 case "list_destroyed":
2273                 case "list_updated":
2274                 case "list_user_subscribed":
2275                 case "list_user_unsubscribed":
2276                     var listEvent = TwitterStreamEvent<TwitterList>.ParseJson(content);
2277                     evt.Target = listEvent.TargetObject.FullName;
2278                     break;
2279                 case "block":
2280                     if (!TabInformations.GetInstance().BlockIds.Contains(eventData.Target.Id)) TabInformations.GetInstance().BlockIds.Add(eventData.Target.Id);
2281                     evt.Target = "";
2282                     break;
2283                 case "unblock":
2284                     if (TabInformations.GetInstance().BlockIds.Contains(eventData.Target.Id)) TabInformations.GetInstance().BlockIds.Remove(eventData.Target.Id);
2285                     evt.Target = "";
2286                     break;
2287                 case "user_update":
2288                     evt.Target = "";
2289                     break;
2290                 
2291                 // Mute / Unmute
2292                 case "mute":
2293                     evt.Target = "@" + eventData.Target.ScreenName;
2294                     if (!TabInformations.GetInstance().MuteUserIds.Contains(eventData.Target.Id))
2295                     {
2296                         TabInformations.GetInstance().MuteUserIds.Add(eventData.Target.Id);
2297                     }
2298                     break;
2299                 case "unmute":
2300                     evt.Target = "@" + eventData.Target.ScreenName;
2301                     if (TabInformations.GetInstance().MuteUserIds.Contains(eventData.Target.Id))
2302                     {
2303                         TabInformations.GetInstance().MuteUserIds.Remove(eventData.Target.Id);
2304                     }
2305                     break;
2306
2307                 default:
2308                     MyCommon.TraceOut("Unknown Event:" + evt.Event + Environment.NewLine + content);
2309                     break;
2310             }
2311             this.StoredEvent.Insert(0, evt);
2312
2313             this.UserStreamEventReceived?.Invoke(this, new UserStreamEventReceivedEventArgs(evt));
2314         }
2315
2316         private void userStream_Started()
2317         {
2318             this.UserStreamStarted?.Invoke(this, EventArgs.Empty);
2319         }
2320
2321         private void userStream_Stopped()
2322         {
2323             this.UserStreamStopped?.Invoke(this, EventArgs.Empty);
2324         }
2325
2326         public bool UserStreamActive
2327             => this.userStream == null ? false : this.userStream.IsStreamActive;
2328
2329         public void StartUserStream()
2330         {
2331             var newStream = new TwitterUserstream(this.Api);
2332
2333             newStream.StatusArrived += userStream_StatusArrived;
2334             newStream.Started += userStream_Started;
2335             newStream.Stopped += userStream_Stopped;
2336
2337             newStream.Start(this.AllAtReply, this.TrackWord);
2338
2339             var oldStream = Interlocked.Exchange(ref this.userStream, newStream);
2340             oldStream?.Dispose();
2341         }
2342
2343         public void StopUserStream()
2344         {
2345             var oldStream = Interlocked.Exchange(ref this.userStream, null);
2346             oldStream?.Dispose();
2347         }
2348
2349         public void ReconnectUserStream()
2350         {
2351             this.StartUserStream();
2352         }
2353
2354         private class TwitterUserstream : IDisposable
2355         {
2356             public bool AllAtReplies { get; private set; }
2357             public string TrackWords { get; private set; }
2358
2359             public bool IsStreamActive { get; private set; }
2360
2361             public event Action<string> StatusArrived;
2362             public event Action Stopped;
2363             public event Action Started;
2364
2365             private TwitterApi twitterApi;
2366
2367             private Task streamTask;
2368             private CancellationTokenSource streamCts;
2369
2370             public TwitterUserstream(TwitterApi twitterApi)
2371             {
2372                 this.twitterApi = twitterApi;
2373             }
2374
2375             public void Start(bool allAtReplies, string trackwords)
2376             {
2377                 this.AllAtReplies = allAtReplies;
2378                 this.TrackWords = trackwords;
2379
2380                 var cts = new CancellationTokenSource();
2381
2382                 this.streamCts = cts;
2383                 this.streamTask = Task.Run(async () =>
2384                 {
2385                     try
2386                     {
2387                         await this.UserStreamLoop(cts.Token)
2388                             .ConfigureAwait(false);
2389                     }
2390                     catch (OperationCanceledException) { }
2391                 });
2392             }
2393
2394             public void Stop()
2395             {
2396                 this.streamCts?.Cancel();
2397
2398                 // streamTask の完了を待たずに IsStreamActive を false にセットする
2399                 this.IsStreamActive = false;
2400                 this.Stopped?.Invoke();
2401             }
2402
2403             private async Task UserStreamLoop(CancellationToken cancellationToken)
2404             {
2405                 TimeSpan? sleep = null;
2406                 for (;;)
2407                 {
2408                     if (sleep != null)
2409                     {
2410                         await Task.Delay(sleep.Value, cancellationToken)
2411                             .ConfigureAwait(false);
2412                         sleep = null;
2413                     }
2414
2415                     if (!MyCommon.IsNetworkAvailable())
2416                     {
2417                         sleep = TimeSpan.FromSeconds(30);
2418                         continue;
2419                     }
2420
2421                     this.IsStreamActive = true;
2422                     this.Started?.Invoke();
2423
2424                     try
2425                     {
2426                         var replies = this.AllAtReplies ? "all" : null;
2427
2428                         using (var stream = await this.twitterApi.UserStreams(replies, this.TrackWords)
2429                             .ConfigureAwait(false))
2430                         using (var reader = new StreamReader(stream))
2431                         {
2432                             while (!reader.EndOfStream)
2433                             {
2434                                 var line = await reader.ReadLineAsync()
2435                                     .ConfigureAwait(false);
2436
2437                                 cancellationToken.ThrowIfCancellationRequested();
2438
2439                                 this.StatusArrived?.Invoke(line);
2440                             }
2441                         }
2442
2443                         // キャンセルされていないのにストリームが終了した場合
2444                         sleep = TimeSpan.FromSeconds(30);
2445                     }
2446                     catch (TwitterApiException) { sleep = TimeSpan.FromSeconds(30); }
2447                     catch (IOException) { sleep = TimeSpan.FromSeconds(30); }
2448                     catch (OperationCanceledException)
2449                     {
2450                         if (cancellationToken.IsCancellationRequested)
2451                             throw;
2452
2453                         // cancellationToken によるキャンセルではない(=タイムアウトエラー)
2454                         sleep = TimeSpan.FromSeconds(30);
2455                     }
2456                     catch (Exception ex)
2457                     {
2458                         MyCommon.ExceptionOut(ex);
2459                         sleep = TimeSpan.FromSeconds(30);
2460                     }
2461                     finally
2462                     {
2463                         this.IsStreamActive = false;
2464                         this.Stopped?.Invoke();
2465                     }
2466                 }
2467             }
2468
2469             private bool disposed = false;
2470
2471             public void Dispose()
2472             {
2473                 if (this.disposed)
2474                     return;
2475
2476                 this.disposed = true;
2477
2478                 this.Stop();
2479
2480                 this.Started = null;
2481                 this.Stopped = null;
2482                 this.StatusArrived = null;
2483             }
2484         }
2485 #endregion
2486
2487 #region "IDisposable Support"
2488         private bool disposedValue; // 重複する呼び出しを検出するには
2489
2490         // IDisposable
2491         protected virtual void Dispose(bool disposing)
2492         {
2493             if (!this.disposedValue)
2494             {
2495                 if (disposing)
2496                 {
2497                     this.StopUserStream();
2498                 }
2499             }
2500             this.disposedValue = true;
2501         }
2502
2503         //protected Overrides void Finalize()
2504         //{
2505         //    // このコードを変更しないでください。クリーンアップ コードを上の Dispose(bool disposing) に記述します。
2506         //    Dispose(false)
2507         //    MyBase.Finalize()
2508         //}
2509
2510         // このコードは、破棄可能なパターンを正しく実装できるように Visual Basic によって追加されました。
2511         public void Dispose()
2512         {
2513             // このコードを変更しないでください。クリーンアップ コードを上の Dispose(bool disposing) に記述します。
2514             Dispose(true);
2515             GC.SuppressFinalize(this);
2516         }
2517 #endregion
2518     }
2519
2520     public class PostDeletedEventArgs : EventArgs
2521     {
2522         public long StatusId { get; }
2523
2524         public PostDeletedEventArgs(long statusId)
2525         {
2526             this.StatusId = statusId;
2527         }
2528     }
2529
2530     public class UserStreamEventReceivedEventArgs : EventArgs
2531     {
2532         public Twitter.FormattedEvent EventData { get; }
2533
2534         public UserStreamEventReceivedEventArgs(Twitter.FormattedEvent eventData)
2535         {
2536             this.EventData = eventData;
2537         }
2538     }
2539 }