OSDN Git Service

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