OSDN Git Service

リプライ先のツイートも発言詳細部に表示させる
[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.Runtime.CompilerServices;
33 using System.Runtime.Serialization;
34 using System.Runtime.Serialization.Json;
35 using System.Text;
36 using System.Text.RegularExpressions;
37 using System.Threading;
38 using System.Threading.Tasks;
39 using System.Web;
40 using System.Xml;
41 using System.Xml.Linq;
42 using System.Xml.XPath;
43 using System;
44 using System.Reflection;
45 using System.Collections.Generic;
46 using System.Drawing;
47 using System.Windows.Forms;
48 using OpenTween.Api;
49 using OpenTween.Connection;
50
51 namespace OpenTween
52 {
53     public class Twitter : IDisposable
54     {
55         #region Regexp from twitter-text-js
56
57         // The code in this region code block incorporates works covered by
58         // the following copyright and permission notices:
59         //
60         //   Copyright 2011 Twitter, Inc.
61         //
62         //   Licensed under the Apache License, Version 2.0 (the "License"); you
63         //   may not use this work except in compliance with the License. You
64         //   may obtain a copy of the License in the LICENSE file, or at:
65         //
66         //   http://www.apache.org/licenses/LICENSE-2.0
67         //
68         //   Unless required by applicable law or agreed to in writing, software
69         //   distributed under the License is distributed on an "AS IS" BASIS,
70         //   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
71         //   implied. See the License for the specific language governing
72         //   permissions and limitations under the License.
73
74         //Hashtag用正規表現
75         private const string LATIN_ACCENTS = @"\u00c0-\u00d6\u00d8-\u00f6\u00f8-\u00ff\u0100-\u024f\u0253\u0254\u0256\u0257\u0259\u025b\u0263\u0268\u026f\u0272\u0289\u028b\u02bb\u1e00-\u1eff";
76         private const string NON_LATIN_HASHTAG_CHARS = @"\u0400-\u04ff\u0500-\u0527\u1100-\u11ff\u3130-\u3185\uA960-\uA97F\uAC00-\uD7AF\uD7B0-\uD7FF";
77         //private const string CJ_HASHTAG_CHARACTERS = @"\u30A1-\u30FA\uFF66-\uFF9F\uFF10-\uFF19\uFF21-\uFF3A\uFF41-\uFF5A\u3041-\u3096\u3400-\u4DBF\u4E00-\u9FFF\u20000-\u2A6DF\u2A700-\u2B73F\u2B740-\u2B81F\u2F800-\u2FA1F";
78         private const string CJ_HASHTAG_CHARACTERS = @"\u30A1-\u30FA\u30FC\u3005\uFF66-\uFF9F\uFF10-\uFF19\uFF21-\uFF3A\uFF41-\uFF5A\u3041-\u309A\u3400-\u4DBF\p{IsCJKUnifiedIdeographs}";
79         private const string HASHTAG_BOUNDARY = @"^|$|\s|「|」|。|\.|!";
80         private const string HASHTAG_ALPHA = "[a-z_" + LATIN_ACCENTS + NON_LATIN_HASHTAG_CHARS + CJ_HASHTAG_CHARACTERS + "]";
81         private const string HASHTAG_ALPHANUMERIC = "[a-z0-9_" + LATIN_ACCENTS + NON_LATIN_HASHTAG_CHARS + CJ_HASHTAG_CHARACTERS + "]";
82         private const string HASHTAG_TERMINATOR = "[^a-z0-9_" + LATIN_ACCENTS + NON_LATIN_HASHTAG_CHARS + CJ_HASHTAG_CHARACTERS + "]";
83         public const string HASHTAG = "(" + HASHTAG_BOUNDARY + ")(#|#)(" + HASHTAG_ALPHANUMERIC + "*" + HASHTAG_ALPHA + HASHTAG_ALPHANUMERIC + "*)(?=" + HASHTAG_TERMINATOR + "|" + HASHTAG_BOUNDARY + ")";
84         //URL正規表現
85         private const string url_valid_preceding_chars = @"(?:[^A-Za-z0-9@@$##\ufffe\ufeff\uffff\u202a-\u202e]|^)";
86         public const string url_invalid_without_protocol_preceding_chars = @"[-_./]$";
87         private const string url_invalid_domain_chars = @"\!'#%&'\(\)*\+,\\\-\.\/:;<=>\?@\[\]\^_{|}~\$\u2000-\u200a\u0009-\u000d\u0020\u0085\u00a0\u1680\u180e\u2028\u2029\u202f\u205f\u3000\ufffe\ufeff\uffff\u202a-\u202e";
88         private const string url_valid_domain_chars = @"[^" + url_invalid_domain_chars + "]";
89         private const string url_valid_subdomain = @"(?:(?:" + url_valid_domain_chars + @"(?:[_-]|" + url_valid_domain_chars + @")*)?" + url_valid_domain_chars + @"\.)";
90         private const string url_valid_domain_name = @"(?:(?:" + url_valid_domain_chars + @"(?:-|" + url_valid_domain_chars + @")*)?" + url_valid_domain_chars + @"\.)";
91         private const string url_valid_GTLD = @"(?:(?:aero|asia|biz|cat|com|coop|edu|gov|info|int|jobs|mil|mobi|museum|name|net|org|pro|tel|travel|xxx)(?=[^0-9a-zA-Z]|$))";
92         private const string url_valid_CCTLD = @"(?:(?:ac|ad|ae|af|ag|ai|al|am|an|ao|aq|ar|as|at|au|aw|ax|az|ba|bb|bd|be|bf|bg|bh|bi|bj|bm|bn|bo|br|bs|bt|bv|bw|by|bz|ca|cc|cd|cf|cg|ch|ci|ck|cl|cm|cn|co|cr|cs|cu|cv|cx|cy|cz|dd|de|dj|dk|dm|do|dz|ec|ee|eg|eh|er|es|et|eu|fi|fj|fk|fm|fo|fr|ga|gb|gd|ge|gf|gg|gh|gi|gl|gm|gn|gp|gq|gr|gs|gt|gu|gw|gy|hk|hm|hn|hr|ht|hu|id|ie|il|im|in|io|iq|ir|is|it|je|jm|jo|jp|ke|kg|kh|ki|km|kn|kp|kr|kw|ky|kz|la|lb|lc|li|lk|lr|ls|lt|lu|lv|ly|ma|mc|md|me|mg|mh|mk|ml|mm|mn|mo|mp|mq|mr|ms|mt|mu|mv|mw|mx|my|mz|na|nc|ne|nf|ng|ni|nl|no|np|nr|nu|nz|om|pa|pe|pf|pg|ph|pk|pl|pm|pn|pr|ps|pt|pw|py|qa|re|ro|rs|ru|rw|sa|sb|sc|sd|se|sg|sh|si|sj|sk|sl|sm|sn|so|sr|ss|st|su|sv|sy|sz|tc|td|tf|tg|th|tj|tk|tl|tm|tn|to|tp|tr|tt|tv|tw|tz|ua|ug|uk|us|uy|uz|va|vc|ve|vg|vi|vn|vu|wf|ws|ye|yt|za|zm|zw)(?=[^0-9a-zA-Z]|$))";
93         private const string url_valid_punycode = @"(?:xn--[0-9a-z]+)";
94         private const string url_valid_domain = @"(?<domain>" + url_valid_subdomain + "*" + url_valid_domain_name + "(?:" + url_valid_GTLD + "|" + url_valid_CCTLD + ")|" + url_valid_punycode + ")";
95         public const string url_valid_ascii_domain = @"(?:(?:[a-z0-9" + LATIN_ACCENTS + @"]+)\.)+(?:" + url_valid_GTLD + "|" + url_valid_CCTLD + "|" + url_valid_punycode + ")";
96         public const string url_invalid_short_domain = "^" + url_valid_domain_name + url_valid_CCTLD + "$";
97         private const string url_valid_port_number = @"[0-9]+";
98
99         private const string url_valid_general_path_chars = @"[a-z0-9!*';:=+,.$/%#\[\]\-_~|&" + LATIN_ACCENTS + "]";
100         private const string url_balance_parens = @"(?:\(" + url_valid_general_path_chars + @"+\))";
101         private const string url_valid_path_ending_chars = @"(?:[+\-a-z0-9=_#/" + LATIN_ACCENTS + "]|" + url_balance_parens + ")";
102         private const string pth = "(?:" +
103             "(?:" +
104                 url_valid_general_path_chars + "*" +
105                 "(?:" + url_balance_parens + url_valid_general_path_chars + "*)*" +
106                 url_valid_path_ending_chars +
107                 ")|(?:@" + url_valid_general_path_chars + "+/)" +
108             ")";
109         private const string qry = @"(?<query>\?[a-z0-9!?*'();:&=+$/%#\[\]\-_.,~|]*[a-z0-9_&=#/])?";
110         public const string rgUrl = @"(?<before>" + url_valid_preceding_chars + ")" +
111                                     "(?<url>(?<protocol>https?://)?" +
112                                     "(?<domain>" + url_valid_domain + ")" +
113                                     "(?::" + url_valid_port_number + ")?" +
114                                     "(?<path>/" + pth + "*)?" +
115                                     qry +
116                                     ")";
117
118         #endregion
119
120         /// <summary>
121         /// Twitter API のステータスページのURL
122         /// </summary>
123         public const string ServiceAvailabilityStatusUrl = "https://status.io.watchmouse.com/7617";
124
125         /// <summary>
126         /// ツイートへのパーマリンクURLを判定する正規表現
127         /// </summary>
128         public static readonly Regex StatusUrlRegex = new Regex(@"https?://([^.]+\.)?twitter\.com/(#!/)?(?<ScreenName>[a-zA-Z0-9_]+)/status(es)?/(?<StatusId>[0-9]+)(/photo)?", RegexOptions.IgnoreCase);
129
130         /// <summary>
131         /// FavstarやaclogなどTwitter関連サービスのパーマリンクURLからステータスIDを抽出する正規表現
132         /// </summary>
133         public static readonly Regex ThirdPartyStatusUrlRegex = new Regex(@"https?://(?:[^.]+\.)?(?:
134   favstar\.fm/users/[a-zA-Z0-9_]+/status/       # Favstar
135 | favstar\.fm/t/                                # Favstar (short)
136 | aclog\.koba789\.com/i/                        # aclog
137 | frtrt\.net/solo_status\.php\?status=          # RtRT
138 )(?<StatusId>[0-9]+)", RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace);
139
140         /// <summary>
141         /// DM送信かどうかを判定する正規表現
142         /// </summary>
143         public static readonly Regex DMSendTextRegex = new Regex(@"^DM? +(?<id>[a-zA-Z0-9_]+) +(?<body>.*)", RegexOptions.IgnoreCase | RegexOptions.Singleline);
144
145         public TwitterConfiguration Configuration { get; private set; }
146
147         delegate void GetIconImageDelegate(PostClass post);
148         private readonly object LockObj = new object();
149         private ISet<long> followerId = new HashSet<long>();
150         private bool _GetFollowerResult = false;
151         private long[] noRTId = new long[0];
152         private bool _GetNoRetweetResult = false;
153
154         //プロパティからアクセスされる共通情報
155         private string _uname;
156
157         private bool _readOwnPost;
158         private List<string> _hashList = new List<string>();
159
160         //max_idで古い発言を取得するために保持(lists分は個別タブで管理)
161         private long minHomeTimeline = long.MaxValue;
162         private long minMentions = long.MaxValue;
163         private long minDirectmessage = long.MaxValue;
164         private long minDirectmessageSent = long.MaxValue;
165
166         //private FavoriteQueue favQueue;
167
168         private HttpTwitter twCon = new HttpTwitter();
169
170         //private List<PostClass> _deletemessages = new List<PostClass>();
171
172         public Twitter()
173         {
174             this.Configuration = TwitterConfiguration.DefaultConfiguration();
175         }
176
177         public TwitterApiAccessLevel AccessLevel
178         {
179             get
180             {
181                 return MyCommon.TwitterApiInfo.AccessLevel;
182             }
183         }
184
185         protected void ResetApiStatus()
186         {
187             MyCommon.TwitterApiInfo.Reset();
188         }
189
190         public void Authenticate(string username, string password)
191         {
192             this.ResetApiStatus();
193
194             HttpStatusCode res;
195             var content = "";
196             try
197             {
198                 res = twCon.AuthUserAndPass(username, password, ref content);
199             }
200             catch(Exception ex)
201             {
202                 throw new WebApiException("Err:" + ex.Message, ex);
203             }
204
205             this.CheckStatusCode(res, content);
206
207             _uname = username.ToLowerInvariant();
208             if (SettingCommon.Instance.UserstreamStartup) this.ReconnectUserStream();
209         }
210
211         public string StartAuthentication()
212         {
213             //OAuth PIN Flow
214             this.ResetApiStatus();
215             try
216             {
217                 string pinPageUrl = null;
218                 var res = twCon.AuthGetRequestToken(ref pinPageUrl);
219                 if (!res)
220                     throw new WebApiException("Err:Failed to access auth server.");
221
222                 return pinPageUrl;
223             }
224             catch (Exception ex)
225             {
226                 throw new WebApiException("Err:Failed to access auth server.", ex);
227             }
228         }
229
230         public void Authenticate(string pinCode)
231         {
232             this.ResetApiStatus();
233
234             HttpStatusCode res;
235             try
236             {
237                 res = twCon.AuthGetAccessToken(pinCode);
238             }
239             catch (Exception ex)
240             {
241                 throw new WebApiException("Err:Failed to access auth acc server.", ex);
242             }
243
244             this.CheckStatusCode(res, null);
245
246             _uname = Username.ToLowerInvariant();
247             if (SettingCommon.Instance.UserstreamStartup) this.ReconnectUserStream();
248         }
249
250         public void ClearAuthInfo()
251         {
252             Twitter.AccountState = MyCommon.ACCOUNT_STATE.Invalid;
253             this.ResetApiStatus();
254             twCon.ClearAuthInfo();
255         }
256
257         public void VerifyCredentials()
258         {
259             HttpStatusCode res;
260             var content = "";
261             try
262             {
263                 res = twCon.VerifyCredentials(ref content);
264             }
265             catch (Exception ex)
266             {
267                 throw new WebApiException("Err:" + ex.Message, ex);
268             }
269
270             this.CheckStatusCode(res, content);
271
272             try
273             {
274                 var user = TwitterUser.ParseJson(content);
275
276                 this.twCon.AuthenticatedUserId = user.Id;
277                 this.UpdateUserStats(user);
278             }
279             catch (SerializationException ex)
280             {
281                 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
282                 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
283             }
284         }
285
286         public void Initialize(string token, string tokenSecret, string username, long userId)
287         {
288             //OAuth認証
289             if (string.IsNullOrEmpty(token) || string.IsNullOrEmpty(tokenSecret) || string.IsNullOrEmpty(username))
290             {
291                 Twitter.AccountState = MyCommon.ACCOUNT_STATE.Invalid;
292             }
293             this.ResetApiStatus();
294             twCon.Initialize(token, tokenSecret, username, userId);
295             _uname = username.ToLowerInvariant();
296             if (SettingCommon.Instance.UserstreamStartup) this.ReconnectUserStream();
297         }
298
299         public string PreProcessUrl(string orgData)
300         {
301             int posl1;
302             var posl2 = 0;
303             //var IDNConveter = new IdnMapping();
304             var href = "<a href=\"";
305
306             while (true)
307             {
308                 if (orgData.IndexOf(href, posl2, StringComparison.Ordinal) > -1)
309                 {
310                     var urlStr = "";
311                     // IDN展開
312                     posl1 = orgData.IndexOf(href, posl2, StringComparison.Ordinal);
313                     posl1 += href.Length;
314                     posl2 = orgData.IndexOf("\"", posl1, StringComparison.Ordinal);
315                     urlStr = orgData.Substring(posl1, posl2 - posl1);
316
317                     if (!urlStr.StartsWith("http://", StringComparison.Ordinal)
318                         && !urlStr.StartsWith("https://", StringComparison.Ordinal)
319                         && !urlStr.StartsWith("ftp://", StringComparison.Ordinal))
320                     {
321                         continue;
322                     }
323
324                     var replacedUrl = MyCommon.IDNEncode(urlStr);
325                     if (replacedUrl == null) continue;
326                     if (replacedUrl == urlStr) continue;
327
328                     orgData = orgData.Replace("<a href=\"" + urlStr, "<a href=\"" + replacedUrl);
329                     posl2 = 0;
330                 }
331                 else
332                 {
333                     break;
334                 }
335             }
336             return orgData;
337         }
338
339         private string GetPlainText(string orgData)
340         {
341             return WebUtility.HtmlDecode(Regex.Replace(orgData, "(?<tagStart><a [^>]+>)(?<text>[^<]+)(?<tagEnd></a>)", "${text}"));
342         }
343
344         // htmlの簡易サニタイズ(詳細表示に不要なタグの除去)
345
346         private string SanitizeHtml(string orgdata)
347         {
348             var retdata = orgdata;
349
350             retdata = Regex.Replace(retdata, "<(script|object|applet|image|frameset|fieldset|legend|style).*" +
351                 "</(script|object|applet|image|frameset|fieldset|legend|style)>", "", RegexOptions.IgnoreCase);
352
353             retdata = Regex.Replace(retdata, "<(frame|link|iframe|img)>", "", RegexOptions.IgnoreCase);
354
355             return retdata;
356         }
357
358         private string AdjustHtml(string orgData)
359         {
360             var retStr = orgData;
361             //var m = Regex.Match(retStr, "<a [^>]+>[#|#](?<1>[a-zA-Z0-9_]+)</a>");
362             //while (m.Success)
363             //{
364             //    lock (LockObj)
365             //    {
366             //        _hashList.Add("#" + m.Groups(1).Value);
367             //    }
368             //    m = m.NextMatch;
369             //}
370             retStr = Regex.Replace(retStr, "<a [^>]*href=\"/", "<a href=\"https://twitter.com/");
371             retStr = retStr.Replace("<a href=", "<a target=\"_self\" href=");
372             retStr = Regex.Replace(retStr, @"(\r\n?|\n)", "<br>"); // CRLF, CR, LF は全て <br> に置換する
373
374             //半角スペースを置換(Thanks @anis774)
375             var ret = false;
376             do
377             {
378                 ret = EscapeSpace(ref retStr);
379             } while (!ret);
380
381             return SanitizeHtml(retStr);
382         }
383
384         private bool EscapeSpace(ref string html)
385         {
386             //半角スペースを置換(Thanks @anis774)
387             var isTag = false;
388             for (int i = 0; i < html.Length; i++)
389             {
390                 if (html[i] == '<')
391                 {
392                     isTag = true;
393                 }
394                 if (html[i] == '>')
395                 {
396                     isTag = false;
397                 }
398
399                 if ((!isTag) && (html[i] == ' '))
400                 {
401                     html = html.Remove(i, 1);
402                     html = html.Insert(i, "&nbsp;");
403                     return false;
404                 }
405             }
406             return true;
407         }
408
409         private struct PostInfo
410         {
411             public string CreatedAt;
412             public string Id;
413             public string Text;
414             public string UserId;
415             public PostInfo(string Created, string IdStr, string txt, string uid)
416             {
417                 CreatedAt = Created;
418                 Id = IdStr;
419                 Text = txt;
420                 UserId = uid;
421             }
422             public bool Equals(PostInfo dst)
423             {
424                 if (this.CreatedAt == dst.CreatedAt && this.Id == dst.Id && this.Text == dst.Text && this.UserId == dst.UserId)
425                 {
426                     return true;
427                 }
428                 else
429                 {
430                     return false;
431                 }
432             }
433         }
434
435         static private PostInfo _prev = new PostInfo("", "", "", "");
436         private bool IsPostRestricted(TwitterStatus status)
437         {
438             var _current = new PostInfo("", "", "", "");
439
440             _current.CreatedAt = status.CreatedAt;
441             _current.Id = status.IdStr;
442             if (status.Text == null)
443             {
444                 _current.Text = "";
445             }
446             else
447             {
448                 _current.Text = status.Text;
449             }
450             _current.UserId = status.User.IdStr;
451
452             if (_current.Equals(_prev))
453             {
454                 return true;
455             }
456             _prev.CreatedAt = _current.CreatedAt;
457             _prev.Id = _current.Id;
458             _prev.Text = _current.Text;
459             _prev.UserId = _current.UserId;
460
461             return false;
462         }
463
464         public void PostStatus(string postStr, long? reply_to, List<long> mediaIds = null)
465         {
466             this.CheckAccountState();
467
468             if (mediaIds == null &&
469                 Twitter.DMSendTextRegex.IsMatch(postStr))
470             {
471                 SendDirectMessage(postStr);
472                 return;
473             }
474
475             HttpStatusCode res;
476             var content = "";
477             try
478             {
479                 res = twCon.UpdateStatus(postStr, reply_to, mediaIds, ref content);
480             }
481             catch(Exception ex)
482             {
483                 throw new WebApiException("Err:" + ex.Message, ex);
484             }
485
486             // 投稿に成功していても404が返ることがあるらしい: https://dev.twitter.com/discussions/1213
487             if (res == HttpStatusCode.NotFound)
488                 return;
489
490             this.CheckStatusCode(res, content);
491
492             TwitterStatus status;
493             try
494             {
495                 status = TwitterStatus.ParseJson(content);
496             }
497             catch(SerializationException ex)
498             {
499                 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
500                 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
501             }
502             catch(Exception ex)
503             {
504                 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
505                 throw new WebApiException("Err:Invalid Json!", content, ex);
506             }
507
508             this.UpdateUserStats(status.User);
509
510             if (IsPostRestricted(status))
511             {
512                 throw new WebApiException("OK:Delaying?");
513             }
514         }
515
516         public void PostStatusWithMedia(string postStr, long? reply_to, IMediaItem item)
517         {
518             this.CheckAccountState();
519
520             HttpStatusCode res;
521             var content = "";
522             try
523             {
524                 res = twCon.UpdateStatusWithMedia(postStr, reply_to, item, ref content);
525             }
526             catch(Exception ex)
527             {
528                 throw new WebApiException("Err:" + ex.Message, ex);
529             }
530
531             // 投稿に成功していても404が返ることがあるらしい: https://dev.twitter.com/discussions/1213
532             if (res == HttpStatusCode.NotFound)
533                 return;
534
535             this.CheckStatusCode(res, content);
536
537             TwitterStatus status;
538             try
539             {
540                 status = TwitterStatus.ParseJson(content);
541             }
542             catch(SerializationException ex)
543             {
544                 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
545                 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
546             }
547             catch(Exception ex)
548             {
549                 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
550                 throw new WebApiException("Err:Invalid Json!", content, ex);
551             }
552
553             this.UpdateUserStats(status.User);
554
555             if (IsPostRestricted(status))
556             {
557                 throw new WebApiException("OK:Delaying?");
558             }
559         }
560
561         public void PostStatusWithMultipleMedia(string postStr, long? reply_to, IMediaItem[] mediaItems)
562         {
563             this.CheckAccountState();
564
565             if (Twitter.DMSendTextRegex.IsMatch(postStr))
566             {
567                 SendDirectMessage(postStr);
568                 return;
569             }
570
571             var mediaIds = new List<long>();
572
573             foreach (var item in mediaItems)
574             {
575                 var mediaId = UploadMedia(item);
576                 mediaIds.Add(mediaId);
577             }
578
579             if (mediaIds.Count == 0)
580                 throw new WebApiException("Err:Invalid Files!");
581
582             PostStatus(postStr, reply_to, mediaIds);
583         }
584
585         public long UploadMedia(IMediaItem item)
586         {
587             this.CheckAccountState();
588
589             HttpStatusCode res;
590             var content = "";
591             try
592             {
593                 res = twCon.UploadMedia(item, ref content);
594             }
595             catch (Exception ex)
596             {
597                 throw new WebApiException("Err:" + ex.Message, ex);
598             }
599
600             this.CheckStatusCode(res, content);
601
602             TwitterUploadMediaResult status;
603             try
604             {
605                 status = TwitterUploadMediaResult.ParseJson(content);
606             }
607             catch (SerializationException ex)
608             {
609                 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
610                 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
611             }
612             catch (Exception ex)
613             {
614                 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
615                 throw new WebApiException("Err:Invalid Json!", content, ex);
616             }
617
618             return status.MediaId;
619         }
620
621         public void SendDirectMessage(string postStr)
622         {
623             this.CheckAccountState();
624             this.CheckAccessLevel(TwitterApiAccessLevel.ReadWriteAndDirectMessage);
625
626             var mc = Twitter.DMSendTextRegex.Match(postStr);
627
628             HttpStatusCode res;
629             var content = "";
630             try
631             {
632                 res = twCon.SendDirectMessage(mc.Groups["body"].Value, mc.Groups["id"].Value, ref content);
633             }
634             catch(Exception ex)
635             {
636                 throw new WebApiException("Err:" + ex.Message, ex);
637             }
638
639             this.CheckStatusCode(res, content);
640
641             TwitterDirectMessage status;
642             try
643             {
644                 status = TwitterDirectMessage.ParseJson(content);
645             }
646             catch(SerializationException ex)
647             {
648                 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
649                 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
650             }
651             catch(Exception ex)
652             {
653                 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
654                 throw new WebApiException("Err:Invalid Json!", content, ex);
655             }
656
657             this.UpdateUserStats(status.Sender);
658         }
659
660         public void RemoveStatus(long id)
661         {
662             this.CheckAccountState();
663
664             HttpStatusCode res;
665             try
666             {
667                 res = twCon.DestroyStatus(id);
668             }
669             catch(Exception ex)
670             {
671                 throw new WebApiException("Err:" + ex.Message, ex);
672             }
673
674             this.CheckStatusCode(res, null);
675         }
676
677         public void PostRetweet(long id, bool read)
678         {
679             this.CheckAccountState();
680
681             //データ部分の生成
682             var target = id;
683             var post = TabInformations.GetInstance()[id];
684             if (post == null)
685             {
686                 throw new WebApiException("Err:Target isn't found.");
687             }
688             if (TabInformations.GetInstance()[id].RetweetedId != null)
689             {
690                 target = TabInformations.GetInstance()[id].RetweetedId.Value; //再RTの場合は元発言をRT
691             }
692
693             HttpStatusCode res;
694             var content = "";
695             try
696             {
697                 res = twCon.RetweetStatus(target, ref content);
698             }
699             catch(Exception ex)
700             {
701                 throw new WebApiException("Err:" + ex.Message, ex);
702             }
703
704             this.CheckStatusCode(res, content);
705
706             TwitterStatus status;
707             try
708             {
709                 status = TwitterStatus.ParseJson(content);
710             }
711             catch(SerializationException ex)
712             {
713                 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
714                 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
715             }
716             catch(Exception ex)
717             {
718                 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
719                 throw new WebApiException("Err:Invalid Json!", content, ex);
720             }
721
722             //ReTweetしたものをTLに追加
723             post = CreatePostsFromStatusData(status);
724             if (post == null)
725                 throw new WebApiException("Invalid Json!", content);
726
727             //二重取得回避
728             lock (LockObj)
729             {
730                 if (TabInformations.GetInstance().ContainsKey(post.StatusId))
731                     return;
732             }
733             //Retweet判定
734             if (post.RetweetedId == null)
735                 throw new WebApiException("Invalid Json!", content);
736             //ユーザー情報
737             post.IsMe = true;
738
739             post.IsRead = read;
740             post.IsOwl = false;
741             if (_readOwnPost) post.IsRead = true;
742             post.IsDm = false;
743
744             TabInformations.GetInstance().AddPost(post);
745         }
746
747         public void RemoveDirectMessage(long id, PostClass post)
748         {
749             this.CheckAccountState();
750             this.CheckAccessLevel(TwitterApiAccessLevel.ReadWriteAndDirectMessage);
751
752             //if (post.IsMe)
753             //    _deletemessages.Add(post)
754             //}
755
756             HttpStatusCode res;
757             try
758             {
759                 res = twCon.DestroyDirectMessage(id);
760             }
761             catch(Exception ex)
762             {
763                 throw new WebApiException("Err:" + ex.Message, ex);
764             }
765
766             this.CheckStatusCode(res, null);
767         }
768
769         public void PostFollowCommand(string screenName)
770         {
771             this.CheckAccountState();
772
773             HttpStatusCode res;
774             var content = "";
775             try
776             {
777                 res = twCon.CreateFriendships(screenName, ref content);
778             }
779             catch(Exception ex)
780             {
781                 throw new WebApiException("Err:" + ex.Message, ex);
782             }
783
784             this.CheckStatusCode(res, content);
785         }
786
787         public void PostRemoveCommand(string screenName)
788         {
789             this.CheckAccountState();
790
791             HttpStatusCode res;
792             var content = "";
793             try
794             {
795                 res = twCon.DestroyFriendships(screenName, ref content);
796             }
797             catch(Exception ex)
798             {
799                 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
800             }
801
802             this.CheckStatusCode(res, content);
803         }
804
805         public void PostCreateBlock(string screenName)
806         {
807             this.CheckAccountState();
808
809             HttpStatusCode res;
810             var content = "";
811             try
812             {
813                 res = twCon.CreateBlock(screenName, ref content);
814             }
815             catch(Exception ex)
816             {
817                 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
818             }
819
820             this.CheckStatusCode(res, content);
821         }
822
823         public void PostDestroyBlock(string screenName)
824         {
825             this.CheckAccountState();
826
827             HttpStatusCode res;
828             var content = "";
829             try
830             {
831                 res = twCon.DestroyBlock(screenName, ref content);
832             }
833             catch(Exception ex)
834             {
835                 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
836             }
837
838             this.CheckStatusCode(res, content);
839         }
840
841         public void PostReportSpam(string screenName)
842         {
843             this.CheckAccountState();
844
845             HttpStatusCode res;
846             var content = "";
847             try
848             {
849                 res = twCon.ReportSpam(screenName, ref content);
850             }
851             catch(Exception ex)
852             {
853                 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
854             }
855
856             this.CheckStatusCode(res, content);
857         }
858
859         public TwitterFriendship GetFriendshipInfo(string screenName)
860         {
861             this.CheckAccountState();
862
863             HttpStatusCode res;
864             var content = "";
865             try
866             {
867                 res = twCon.ShowFriendships(_uname, screenName, ref content);
868             }
869             catch(Exception ex)
870             {
871                 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
872             }
873
874             this.CheckStatusCode(res, content);
875
876             try
877             {
878                 return TwitterFriendship.ParseJson(content);
879             }
880             catch(SerializationException ex)
881             {
882                 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
883                 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
884             }
885             catch(Exception ex)
886             {
887                 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
888                 throw new WebApiException("Err:Invalid Json!", content, ex);
889             }
890         }
891
892         public TwitterUser GetUserInfo(string screenName)
893         {
894             this.CheckAccountState();
895
896             HttpStatusCode res;
897             var content = "";
898             try
899             {
900                 res = twCon.ShowUserInfo(screenName, ref content);
901             }
902             catch(Exception ex)
903             {
904                 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
905             }
906
907             this.CheckStatusCode(res, content);
908
909             try
910             {
911                 return TwitterUser.ParseJson(content);
912             }
913             catch (SerializationException ex)
914             {
915                 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
916                 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
917             }
918             catch (Exception ex)
919             {
920                 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
921                 throw new WebApiException("Err:Invalid Json!", content, ex);
922             }
923         }
924
925         public int GetStatus_Retweeted_Count(long StatusId)
926         {
927             this.CheckAccountState();
928
929             HttpStatusCode res;
930             var content = "";
931             try
932             {
933                 res = twCon.ShowStatuses(StatusId, ref content);
934             }
935             catch (Exception ex)
936             {
937                 throw new WebApiException("Err:" + ex.Message, ex);
938             }
939
940             this.CheckStatusCode(res, content);
941
942             try
943             {
944                 var status = TwitterStatus.ParseJson(content);
945                 return status.RetweetCount;
946             }
947             catch (SerializationException ex)
948             {
949                 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
950                 throw new WebApiException("Json Parse Error(DataContractJsonSerializer)", content, ex);
951             }
952             catch (Exception ex)
953             {
954                 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
955                 throw new WebApiException("Invalid Json!", content, ex);
956             }
957         }
958
959         public void PostFavAdd(long id)
960         {
961             this.CheckAccountState();
962
963             //if (this.favQueue == null) this.favQueue = new FavoriteQueue(this)
964
965             //if (this.favQueue.Contains(id)) this.favQueue.Remove(id)
966
967             HttpStatusCode res;
968             var content = "";
969             try
970             {
971                 res = twCon.CreateFavorites(id, ref content);
972             }
973             catch(Exception ex)
974             {
975                 //this.favQueue.Add(id)
976                 //return "Err:->FavoriteQueue:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")";
977                 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
978             }
979
980             this.CheckStatusCode(res, content);
981
982             if (!RestrictFavCheck)
983                 return;
984
985             //http://twitter.com/statuses/show/id.xml APIを発行して本文を取得
986
987             try
988             {
989                 res = twCon.ShowStatuses(id, ref content);
990             }
991             catch(Exception ex)
992             {
993                 throw new WebApiException("Err:" + ex.Message, ex);
994             }
995
996             this.CheckStatusCode(res, content);
997
998             TwitterStatus status;
999             try
1000             {
1001                 status = TwitterStatus.ParseJson(content);
1002             }
1003             catch (SerializationException ex)
1004             {
1005                 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
1006                 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
1007             }
1008             catch (Exception ex)
1009             {
1010                 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
1011                 throw new WebApiException("Err:Invalid Json!", content, ex);
1012             }
1013             if (status.Favorited != true)
1014                 throw new WebApiException("NG(Restricted?)");
1015         }
1016
1017         public void PostFavRemove(long id)
1018         {
1019             this.CheckAccountState();
1020
1021             //if (this.favQueue == null) this.favQueue = new FavoriteQueue(this)
1022
1023             //if (this.favQueue.Contains(id))
1024             //    this.favQueue.Remove(id)
1025             //    return "";
1026             //}
1027
1028             HttpStatusCode res;
1029             var content = "";
1030             try
1031             {
1032                 res = twCon.DestroyFavorites(id, ref content);
1033             }
1034             catch(Exception ex)
1035             {
1036                 throw new WebApiException("Err:" + ex.Message, ex);
1037             }
1038
1039             this.CheckStatusCode(res, content);
1040         }
1041
1042         public TwitterUser PostUpdateProfile(string name, string url, string location, string description)
1043         {
1044             this.CheckAccountState();
1045
1046             HttpStatusCode res;
1047             var content = "";
1048             try
1049             {
1050                 res = twCon.UpdateProfile(name, url, location, description, ref content);
1051             }
1052             catch(Exception ex)
1053             {
1054                 throw new WebApiException("Err:" + ex.Message, content, ex);
1055             }
1056
1057             this.CheckStatusCode(res, content);
1058
1059             try
1060             {
1061                 return TwitterUser.ParseJson(content);
1062             }
1063             catch (SerializationException e)
1064             {
1065                 var ex = new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, e);
1066                 MyCommon.TraceOut(ex);
1067                 throw ex;
1068             }
1069             catch (Exception e)
1070             {
1071                 var ex = new WebApiException("Err:Invalid Json!", content, e);
1072                 MyCommon.TraceOut(ex);
1073                 throw ex;
1074             }
1075         }
1076
1077         public void PostUpdateProfileImage(string filename)
1078         {
1079             this.CheckAccountState();
1080
1081             HttpStatusCode res;
1082             var content = "";
1083             try
1084             {
1085                 res = twCon.UpdateProfileImage(new FileInfo(filename), ref content);
1086             }
1087             catch(Exception ex)
1088             {
1089                 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
1090             }
1091
1092             this.CheckStatusCode(res, content);
1093         }
1094
1095         public string Username
1096         {
1097             get
1098             {
1099                 return twCon.AuthenticatedUsername;
1100             }
1101         }
1102
1103         public long UserId
1104         {
1105             get
1106             {
1107                 return twCon.AuthenticatedUserId;
1108             }
1109         }
1110
1111         public string Password
1112         {
1113             get
1114             {
1115                 return twCon.Password;
1116             }
1117         }
1118
1119         private static MyCommon.ACCOUNT_STATE _accountState = MyCommon.ACCOUNT_STATE.Valid;
1120         public static MyCommon.ACCOUNT_STATE AccountState
1121         {
1122             get
1123             {
1124                 return _accountState;
1125             }
1126             set
1127             {
1128                 _accountState = value;
1129             }
1130         }
1131
1132         public bool RestrictFavCheck { get; set; }
1133
1134 #region "バージョンアップ"
1135         public void GetTweenBinary(string strVer)
1136         {
1137             try
1138             {
1139                 //本体
1140                 if (!(new HttpVarious()).GetDataToFile("http://tween.sourceforge.jp/Tween" + strVer + ".gz?" + DateTime.Now.ToString("yyMMddHHmmss") + Environment.TickCount.ToString(),
1141                                                     Path.Combine(MyCommon.settingPath, "TweenNew.exe")))
1142                 {
1143                     throw new WebApiException("Err:Download failed");
1144                 }
1145                 //英語リソース
1146                 if (!Directory.Exists(Path.Combine(MyCommon.settingPath, "en")))
1147                 {
1148                     Directory.CreateDirectory(Path.Combine(MyCommon.settingPath, "en"));
1149                 }
1150                 if (!(new HttpVarious()).GetDataToFile("http://tween.sourceforge.jp/TweenResEn" + strVer + ".gz?" + DateTime.Now.ToString("yyMMddHHmmss") + Environment.TickCount.ToString(),
1151                                                     Path.Combine(Path.Combine(MyCommon.settingPath, "en"), "Tween.resourcesNew.dll")))
1152                 {
1153                     throw new WebApiException("Err:Download failed");
1154                 }
1155                 //その他言語圏のリソース。取得失敗しても継続
1156                 //UIの言語圏のリソース
1157                 var curCul = "";
1158                 if (!Thread.CurrentThread.CurrentUICulture.IsNeutralCulture)
1159                 {
1160                     var idx = Thread.CurrentThread.CurrentUICulture.Name.LastIndexOf('-');
1161                     if (idx > -1)
1162                     {
1163                         curCul = Thread.CurrentThread.CurrentUICulture.Name.Substring(0, idx);
1164                     }
1165                     else
1166                     {
1167                         curCul = Thread.CurrentThread.CurrentUICulture.Name;
1168                     }
1169                 }
1170                 else
1171                 {
1172                     curCul = Thread.CurrentThread.CurrentUICulture.Name;
1173                 }
1174                 if (!string.IsNullOrEmpty(curCul) && curCul != "en" && curCul != "ja")
1175                 {
1176                     if (!Directory.Exists(Path.Combine(MyCommon.settingPath, curCul)))
1177                     {
1178                         Directory.CreateDirectory(Path.Combine(MyCommon.settingPath, curCul));
1179                     }
1180                     if (!(new HttpVarious()).GetDataToFile("http://tween.sourceforge.jp/TweenRes" + curCul + strVer + ".gz?" + DateTime.Now.ToString("yyMMddHHmmss") + Environment.TickCount.ToString(),
1181                                                         Path.Combine(Path.Combine(MyCommon.settingPath, curCul), "Tween.resourcesNew.dll")))
1182                     {
1183                         //return "Err:Download failed";
1184                     }
1185                 }
1186                 //スレッドの言語圏のリソース
1187                 string curCul2;
1188                 if (!Thread.CurrentThread.CurrentCulture.IsNeutralCulture)
1189                 {
1190                     var idx = Thread.CurrentThread.CurrentCulture.Name.LastIndexOf('-');
1191                     if (idx > -1)
1192                     {
1193                         curCul2 = Thread.CurrentThread.CurrentCulture.Name.Substring(0, idx);
1194                     }
1195                     else
1196                     {
1197                         curCul2 = Thread.CurrentThread.CurrentCulture.Name;
1198                     }
1199                 }
1200                 else
1201                 {
1202                     curCul2 = Thread.CurrentThread.CurrentCulture.Name;
1203                 }
1204                 if (!string.IsNullOrEmpty(curCul2) && curCul2 != "en" && curCul2 != curCul)
1205                 {
1206                     if (!Directory.Exists(Path.Combine(MyCommon.settingPath, curCul2)))
1207                     {
1208                         Directory.CreateDirectory(Path.Combine(MyCommon.settingPath, curCul2));
1209                     }
1210                     if (!(new HttpVarious()).GetDataToFile("http://tween.sourceforge.jp/TweenRes" + curCul2 + strVer + ".gz?" + DateTime.Now.ToString("yyMMddHHmmss") + Environment.TickCount.ToString(),
1211                                                     Path.Combine(Path.Combine(MyCommon.settingPath, curCul2), "Tween.resourcesNew.dll")))
1212                     {
1213                         //return "Err:Download failed";
1214                     }
1215                 }
1216
1217                 //アップデータ
1218                 if (!(new HttpVarious()).GetDataToFile("http://tween.sourceforge.jp/TweenUp3.gz?" + DateTime.Now.ToString("yyMMddHHmmss") + Environment.TickCount.ToString(),
1219                                                     Path.Combine(MyCommon.settingPath, "TweenUp3.exe")))
1220                 {
1221                     throw new WebApiException("Err:Download failed");
1222                 }
1223                 //シリアライザDLL
1224                 if (!(new HttpVarious()).GetDataToFile("http://tween.sourceforge.jp/TweenDll" + strVer + ".gz?" + DateTime.Now.ToString("yyMMddHHmmss") + Environment.TickCount.ToString(),
1225                                                     Path.Combine(MyCommon.settingPath, "TweenNew.XmlSerializers.dll")))
1226                 {
1227                     throw new WebApiException("Err:Download failed");
1228                 }
1229             }
1230             catch (Exception ex)
1231             {
1232                 throw new WebApiException("Err:Download failed", ex);
1233             }
1234         }
1235 #endregion
1236
1237         public bool ReadOwnPost
1238         {
1239             get
1240             {
1241                 return _readOwnPost;
1242             }
1243             set
1244             {
1245                 _readOwnPost = value;
1246             }
1247         }
1248
1249         public int FollowersCount { get; private set; }
1250         public int FriendsCount { get; private set; }
1251         public int StatusesCount { get; private set; }
1252         public string Location { get; private set; } = "";
1253         public string Bio { get; private set; } = "";
1254
1255         /// <summary>ユーザーのフォロワー数などの情報を更新します</summary>
1256         private void UpdateUserStats(TwitterUser self)
1257         {
1258             this.FollowersCount = self.FollowersCount;
1259             this.FriendsCount = self.FriendsCount;
1260             this.StatusesCount = self.StatusesCount;
1261             this.Location = self.Location;
1262             this.Bio = self.Description;
1263         }
1264
1265         /// <summary>
1266         /// 渡された取得件数がWORKERTYPEに応じた取得可能範囲に収まっているか検証する
1267         /// </summary>
1268         public static bool VerifyApiResultCount(MyCommon.WORKERTYPE type, int count)
1269         {
1270             return count >= 20 && count <= GetMaxApiResultCount(type);
1271         }
1272
1273         /// <summary>
1274         /// 渡された取得件数が更新時の取得可能範囲に収まっているか検証する
1275         /// </summary>
1276         public static bool VerifyMoreApiResultCount(int count)
1277         {
1278             return count >= 20 && count <= 200;
1279         }
1280
1281         /// <summary>
1282         /// 渡された取得件数が起動時の取得可能範囲に収まっているか検証する
1283         /// </summary>
1284         public static bool VerifyFirstApiResultCount(int count)
1285         {
1286             return count >= 20 && count <= 200;
1287         }
1288
1289         /// <summary>
1290         /// WORKERTYPEに応じた取得可能な最大件数を取得する
1291         /// </summary>
1292         public static int GetMaxApiResultCount(MyCommon.WORKERTYPE type)
1293         {
1294             // 参照: REST APIs - 各endpointのcountパラメータ
1295             // https://dev.twitter.com/rest/public
1296             switch (type)
1297             {
1298                 case MyCommon.WORKERTYPE.Timeline:
1299                 case MyCommon.WORKERTYPE.Reply:
1300                 case MyCommon.WORKERTYPE.UserTimeline:
1301                 case MyCommon.WORKERTYPE.Favorites:
1302                 case MyCommon.WORKERTYPE.DirectMessegeRcv:
1303                 case MyCommon.WORKERTYPE.DirectMessegeSnt:
1304                 case MyCommon.WORKERTYPE.List:  // 不明
1305                     return 200;
1306
1307                 case MyCommon.WORKERTYPE.PublicSearch:
1308                     return 100;
1309
1310                 default:
1311                     throw new InvalidOperationException("Invalid type: " + type);
1312             }
1313         }
1314
1315         /// <summary>
1316         /// WORKERTYPEに応じた取得件数を取得する
1317         /// </summary>
1318         public static int GetApiResultCount(MyCommon.WORKERTYPE type, bool more, bool startup)
1319         {
1320             if (type == MyCommon.WORKERTYPE.DirectMessegeRcv ||
1321                 type == MyCommon.WORKERTYPE.DirectMessegeSnt)
1322             {
1323                 return 20;
1324             }
1325
1326             if (SettingCommon.Instance.UseAdditionalCount)
1327             {
1328                 switch (type)
1329                 {
1330                     case MyCommon.WORKERTYPE.Favorites:
1331                         if (SettingCommon.Instance.FavoritesCountApi != 0)
1332                             return SettingCommon.Instance.FavoritesCountApi;
1333                         break;
1334                     case MyCommon.WORKERTYPE.List:
1335                         if (SettingCommon.Instance.ListCountApi != 0)
1336                             return SettingCommon.Instance.ListCountApi;
1337                         break;
1338                     case MyCommon.WORKERTYPE.PublicSearch:
1339                         if (SettingCommon.Instance.SearchCountApi != 0)
1340                             return SettingCommon.Instance.SearchCountApi;
1341                         break;
1342                     case MyCommon.WORKERTYPE.UserTimeline:
1343                         if (SettingCommon.Instance.UserTimelineCountApi != 0)
1344                             return SettingCommon.Instance.UserTimelineCountApi;
1345                         break;
1346                 }
1347                 if (more && SettingCommon.Instance.MoreCountApi != 0)
1348                 {
1349                     return Math.Min(SettingCommon.Instance.MoreCountApi, GetMaxApiResultCount(type));
1350                 }
1351                 if (startup && SettingCommon.Instance.FirstCountApi != 0 && type != MyCommon.WORKERTYPE.Reply)
1352                 {
1353                     return Math.Min(SettingCommon.Instance.FirstCountApi, GetMaxApiResultCount(type));
1354                 }
1355             }
1356
1357             // 上記に当てはまらない場合の共通処理
1358             var count = SettingCommon.Instance.CountApi;
1359
1360             if (type == MyCommon.WORKERTYPE.Reply)
1361                 count = SettingCommon.Instance.CountApiReply;
1362
1363             return Math.Min(count, GetMaxApiResultCount(type));
1364         }
1365
1366         public void GetTimelineApi(bool read,
1367                                 MyCommon.WORKERTYPE gType,
1368                                 bool more,
1369                                 bool startup)
1370         {
1371             this.CheckAccountState();
1372
1373             HttpStatusCode res;
1374             var content = "";
1375             var count = GetApiResultCount(gType, more, startup);
1376
1377             try
1378             {
1379                 if (gType == MyCommon.WORKERTYPE.Timeline)
1380                 {
1381                     if (more)
1382                     {
1383                         res = twCon.HomeTimeline(count, this.minHomeTimeline, null, ref content);
1384                     }
1385                     else
1386                     {
1387                         res = twCon.HomeTimeline(count, null, null, ref content);
1388                     }
1389                 }
1390                 else
1391                 {
1392                     if (more)
1393                     {
1394                         res = twCon.Mentions(count, this.minMentions, null, ref content);
1395                     }
1396                     else
1397                     {
1398                         res = twCon.Mentions(count, null, null, ref content);
1399                     }
1400                 }
1401             }
1402             catch(Exception ex)
1403             {
1404                 throw new WebApiException("Err:" + ex.Message, ex);
1405             }
1406
1407             this.CheckStatusCode(res, content);
1408
1409             var minimumId = CreatePostsFromJson(content, gType, null, read);
1410
1411             if (minimumId != null)
1412             {
1413                 if (gType == MyCommon.WORKERTYPE.Timeline)
1414                     this.minHomeTimeline = minimumId.Value;
1415                 else
1416                     this.minMentions = minimumId.Value;
1417             }
1418         }
1419
1420         public void GetUserTimelineApi(bool read,
1421                                          string userName,
1422                                          TabClass tab,
1423                                          bool more)
1424         {
1425             this.CheckAccountState();
1426
1427             HttpStatusCode res;
1428             var content = "";
1429             var count = GetApiResultCount(MyCommon.WORKERTYPE.UserTimeline, more, false);
1430
1431             try
1432             {
1433                 if (string.IsNullOrEmpty(userName))
1434                 {
1435                     var target = tab.User;
1436                     if (string.IsNullOrEmpty(target)) return;
1437                     userName = target;
1438                     res = twCon.UserTimeline(null, target, count, null, null, ref content);
1439                 }
1440                 else
1441                 {
1442                     if (more)
1443                     {
1444                         res = twCon.UserTimeline(null, userName, count, tab.OldestId, null, ref content);
1445                     }
1446                     else
1447                     {
1448                         res = twCon.UserTimeline(null, userName, count, null, null, ref content);
1449                     }
1450                 }
1451             }
1452             catch(Exception ex)
1453             {
1454                 throw new WebApiException("Err:" + ex.Message, ex);
1455             }
1456
1457             if (res == HttpStatusCode.Unauthorized)
1458                 throw new WebApiException("Err:@" + userName + "'s Tweets are protected.");
1459
1460             this.CheckStatusCode(res, content);
1461
1462             var minimumId = CreatePostsFromJson(content, MyCommon.WORKERTYPE.UserTimeline, tab, read);
1463
1464             if (minimumId != null)
1465                 tab.OldestId = minimumId.Value;
1466         }
1467
1468         public PostClass GetStatusApi(bool read, long id)
1469         {
1470             this.CheckAccountState();
1471
1472             HttpStatusCode res;
1473             var content = "";
1474             try
1475             {
1476                 res = twCon.ShowStatuses(id, ref content);
1477             }
1478             catch(Exception ex)
1479             {
1480                 throw new WebApiException("Err:" + ex.Message, ex);
1481             }
1482
1483             if (res == HttpStatusCode.Forbidden)
1484                 throw new WebApiException("Err:protected user's tweet", content);
1485
1486             this.CheckStatusCode(res, content);
1487
1488             TwitterStatus status;
1489             try
1490             {
1491                 status = TwitterStatus.ParseJson(content);
1492             }
1493             catch(SerializationException ex)
1494             {
1495                 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
1496                 throw new WebApiException("Json Parse Error(DataContractJsonSerializer)", content, ex);
1497             }
1498             catch(Exception ex)
1499             {
1500                 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
1501                 throw new WebApiException("Invalid Json!", content, ex);
1502             }
1503
1504             var item = CreatePostsFromStatusData(status);
1505             if (item == null)
1506                 throw new WebApiException("Err:Can't create post", content);
1507
1508             item.IsRead = read;
1509             if (item.IsMe && !read && _readOwnPost) item.IsRead = true;
1510
1511             return item;
1512         }
1513
1514         public void GetStatusApi(bool read, long id, TabClass tab)
1515         {
1516             var post = this.GetStatusApi(read, id);
1517
1518             //非同期アイコン取得&StatusDictionaryに追加
1519             if (tab != null && tab.IsInnerStorageTabType)
1520                 tab.AddPostToInnerStorage(post);
1521             else
1522                 TabInformations.GetInstance().AddPost(post);
1523         }
1524
1525         private PostClass CreatePostsFromStatusData(TwitterStatus status)
1526         {
1527             return CreatePostsFromStatusData(status, false);
1528         }
1529
1530         private PostClass CreatePostsFromStatusData(TwitterStatus status, bool favTweet)
1531         {
1532             var post = new PostClass();
1533             TwitterEntities entities;
1534             string sourceHtml;
1535
1536             post.StatusId = status.Id;
1537             if (status.RetweetedStatus != null)
1538             {
1539                 var retweeted = status.RetweetedStatus;
1540
1541                 post.CreatedAt = MyCommon.DateTimeParse(retweeted.CreatedAt);
1542
1543                 //Id
1544                 post.RetweetedId = retweeted.Id;
1545                 //本文
1546                 post.TextFromApi = retweeted.Text;
1547                 entities = retweeted.MergedEntities;
1548                 sourceHtml = retweeted.Source;
1549                 //Reply先
1550                 post.InReplyToStatusId = retweeted.InReplyToStatusId;
1551                 post.InReplyToUser = retweeted.InReplyToScreenName;
1552                 post.InReplyToUserId = status.InReplyToUserId;
1553
1554                 if (favTweet)
1555                 {
1556                     post.IsFav = true;
1557                 }
1558                 else
1559                 {
1560                     //幻覚fav対策
1561                     var tc = TabInformations.GetInstance().GetTabByType(MyCommon.TabUsageType.Favorites);
1562                     post.IsFav = tc.Contains(retweeted.Id);
1563                 }
1564
1565                 if (retweeted.Coordinates != null)
1566                     post.PostGeo = new PostClass.StatusGeo(retweeted.Coordinates.Coordinates[0], retweeted.Coordinates.Coordinates[1]);
1567
1568                 //以下、ユーザー情報
1569                 var user = retweeted.User;
1570
1571                 if (user == null || user.ScreenName == null || status.User.ScreenName == null) return null;
1572
1573                 post.UserId = user.Id;
1574                 post.ScreenName = user.ScreenName;
1575                 post.Nickname = user.Name.Trim();
1576                 post.ImageUrl = user.ProfileImageUrlHttps;
1577                 post.IsProtect = user.Protected;
1578
1579                 //Retweetした人
1580                 post.RetweetedBy = status.User.ScreenName;
1581                 post.RetweetedByUserId = status.User.Id;
1582                 post.IsMe = post.RetweetedBy.ToLowerInvariant().Equals(_uname);
1583             }
1584             else
1585             {
1586                 post.CreatedAt = MyCommon.DateTimeParse(status.CreatedAt);
1587                 //本文
1588                 post.TextFromApi = status.Text;
1589                 entities = status.MergedEntities;
1590                 sourceHtml = status.Source;
1591                 post.InReplyToStatusId = status.InReplyToStatusId;
1592                 post.InReplyToUser = status.InReplyToScreenName;
1593                 post.InReplyToUserId = status.InReplyToUserId;
1594
1595                 if (favTweet)
1596                 {
1597                     post.IsFav = true;
1598                 }
1599                 else
1600                 {
1601                     //幻覚fav対策
1602                     var tc = TabInformations.GetInstance().GetTabByType(MyCommon.TabUsageType.Favorites);
1603                     post.IsFav = tc.Contains(post.StatusId) && TabInformations.GetInstance()[post.StatusId].IsFav;
1604                 }
1605
1606                 if (status.Coordinates != null)
1607                     post.PostGeo = new PostClass.StatusGeo(status.Coordinates.Coordinates[0], status.Coordinates.Coordinates[1]);
1608
1609                 //以下、ユーザー情報
1610                 var user = status.User;
1611
1612                 if (user == null || user.ScreenName == null) return null;
1613
1614                 post.UserId = user.Id;
1615                 post.ScreenName = user.ScreenName;
1616                 post.Nickname = user.Name.Trim();
1617                 post.ImageUrl = user.ProfileImageUrlHttps;
1618                 post.IsProtect = user.Protected;
1619                 post.IsMe = post.ScreenName.ToLowerInvariant().Equals(_uname);
1620             }
1621             //HTMLに整形
1622             string textFromApi = post.TextFromApi;
1623             post.Text = CreateHtmlAnchor(textFromApi, post.ReplyToList, entities, post.Media);
1624             post.TextFromApi = textFromApi;
1625             post.TextFromApi = this.ReplaceTextFromApi(post.TextFromApi, entities);
1626             post.TextFromApi = WebUtility.HtmlDecode(post.TextFromApi);
1627             post.TextFromApi = post.TextFromApi.Replace("<3", "\u2661");
1628
1629             var quoteStatusIds = GetQuoteTweetStatusIds(entities);
1630
1631             if (post.InReplyToStatusId != null)
1632                 quoteStatusIds = quoteStatusIds.Concat(new[] { post.InReplyToStatusId.Value });
1633
1634             post.QuoteStatusIds = quoteStatusIds
1635                 .Where(x => x != post.StatusId && x != post.RetweetedId)
1636                 .Distinct().ToArray();
1637
1638             post.ExpandedUrls = entities.OfType<TwitterEntityUrl>()
1639                 .Select(x => new PostClass.ExpandedUrlInfo(x.Url, x.ExpandedUrl))
1640                 .ToArray();
1641
1642             //Source整形
1643             var source = ParseSource(sourceHtml);
1644             post.Source = source.Item1;
1645             post.SourceUri = source.Item2;
1646
1647             post.IsReply = post.ReplyToList.Contains(_uname);
1648             post.IsExcludeReply = false;
1649
1650             if (post.IsMe)
1651             {
1652                 post.IsOwl = false;
1653             }
1654             else
1655             {
1656                 if (followerId.Count > 0) post.IsOwl = !followerId.Contains(post.UserId);
1657             }
1658
1659             post.IsDm = false;
1660             return post;
1661         }
1662
1663         /// <summary>
1664         /// ツイートに含まれる引用ツイートのURLからステータスIDを抽出
1665         /// </summary>
1666         public static IEnumerable<long> GetQuoteTweetStatusIds(IEnumerable<TwitterEntity> entities)
1667         {
1668             var urls = entities.OfType<TwitterEntityUrl>().Select(x => x.ExpandedUrl);
1669
1670             return GetQuoteTweetStatusIds(urls);
1671         }
1672
1673         public static IEnumerable<long> GetQuoteTweetStatusIds(IEnumerable<string> urls)
1674         {
1675             foreach (var url in urls)
1676             {
1677                 var match = Twitter.StatusUrlRegex.Match(url);
1678                 if (match.Success)
1679                 {
1680                     long statusId;
1681                     if (long.TryParse(match.Groups["StatusId"].Value, out statusId))
1682                         yield return statusId;
1683                 }
1684             }
1685         }
1686
1687         private long? CreatePostsFromJson(string content, MyCommon.WORKERTYPE gType, TabClass tab, bool read)
1688         {
1689             TwitterStatus[] items;
1690             try
1691             {
1692                 items = TwitterStatus.ParseJsonArray(content);
1693             }
1694             catch(SerializationException ex)
1695             {
1696                 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
1697                 throw new WebApiException("Json Parse Error(DataContractJsonSerializer)", content, ex);
1698             }
1699             catch(Exception ex)
1700             {
1701                 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
1702                 throw new WebApiException("Invalid Json!", content, ex);
1703             }
1704
1705             long? minimumId = null;
1706
1707             foreach (var status in items)
1708             {
1709                 PostClass post = null;
1710                 post = CreatePostsFromStatusData(status);
1711                 if (post == null) continue;
1712
1713                 if (minimumId == null || minimumId.Value > post.StatusId)
1714                     minimumId = post.StatusId;
1715
1716                 //二重取得回避
1717                 lock (LockObj)
1718                 {
1719                     if (tab == null)
1720                     {
1721                         if (TabInformations.GetInstance().ContainsKey(post.StatusId)) continue;
1722                     }
1723                     else
1724                     {
1725                         if (tab.Contains(post.StatusId)) continue;
1726                     }
1727                 }
1728
1729                 //RT禁止ユーザーによるもの
1730                 if (gType != MyCommon.WORKERTYPE.UserTimeline &&
1731                     post.RetweetedByUserId != null && this.noRTId.Contains(post.RetweetedByUserId.Value)) continue;
1732
1733                 post.IsRead = read;
1734                 if (post.IsMe && !read && _readOwnPost) post.IsRead = true;
1735
1736                 //非同期アイコン取得&StatusDictionaryに追加
1737                 if (tab != null && tab.IsInnerStorageTabType)
1738                     tab.AddPostToInnerStorage(post);
1739                 else
1740                     TabInformations.GetInstance().AddPost(post);
1741             }
1742
1743             return minimumId;
1744         }
1745
1746         private long? CreatePostsFromSearchJson(string content, TabClass tab, bool read, int count, bool more)
1747         {
1748             TwitterSearchResult items;
1749             try
1750             {
1751                 items = TwitterSearchResult.ParseJson(content);
1752             }
1753             catch (SerializationException ex)
1754             {
1755                 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
1756                 throw new WebApiException("Json Parse Error(DataContractJsonSerializer)", content, ex);
1757             }
1758             catch (Exception ex)
1759             {
1760                 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
1761                 throw new WebApiException("Invalid Json!", content, ex);
1762             }
1763
1764             long? minimumId = null;
1765
1766             foreach (var result in items.Statuses)
1767             {
1768                 PostClass post = null;
1769                 post = CreatePostsFromStatusData(result);
1770
1771                 if (post == null)
1772                 {
1773                     // Search API は相変わらずぶっ壊れたデータを返すことがあるため、必要なデータが欠如しているものは取得し直す
1774                     try
1775                     {
1776                         post = this.GetStatusApi(read, result.Id);
1777                     }
1778                     catch (WebApiException)
1779                     {
1780                         continue;
1781                     }
1782                 }
1783
1784                 if (minimumId == null || minimumId.Value > post.StatusId)
1785                     minimumId = post.StatusId;
1786
1787                 if (!more && post.StatusId > tab.SinceId) tab.SinceId = post.StatusId;
1788                 //二重取得回避
1789                 lock (LockObj)
1790                 {
1791                     if (tab == null)
1792                     {
1793                         if (TabInformations.GetInstance().ContainsKey(post.StatusId)) continue;
1794                     }
1795                     else
1796                     {
1797                         if (tab.Contains(post.StatusId)) continue;
1798                     }
1799                 }
1800
1801                 post.IsRead = read;
1802                 if ((post.IsMe && !read) && this._readOwnPost) post.IsRead = true;
1803
1804                 //非同期アイコン取得&StatusDictionaryに追加
1805                 if (tab != null && tab.IsInnerStorageTabType)
1806                     tab.AddPostToInnerStorage(post);
1807                 else
1808                     TabInformations.GetInstance().AddPost(post);
1809             }
1810
1811             return minimumId;
1812         }
1813
1814         private void CreateFavoritePostsFromJson(string content, bool read)
1815         {
1816             TwitterStatus[] item;
1817             try
1818             {
1819                 item = TwitterStatus.ParseJsonArray(content);
1820             }
1821             catch (SerializationException ex)
1822             {
1823                 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
1824                 throw new WebApiException("Json Parse Error(DataContractJsonSerializer)", content, ex);
1825             }
1826             catch (Exception ex)
1827             {
1828                 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
1829                 throw new WebApiException("Invalid Json!", content, ex);
1830             }
1831
1832             var favTab = TabInformations.GetInstance().GetTabByType(MyCommon.TabUsageType.Favorites);
1833
1834             foreach (var status in item)
1835             {
1836                 //二重取得回避
1837                 lock (LockObj)
1838                 {
1839                     if (favTab.Contains(status.Id)) continue;
1840                 }
1841
1842                 var post = CreatePostsFromStatusData(status, true);
1843                 if (post == null) continue;
1844
1845                 post.IsRead = read;
1846
1847                 TabInformations.GetInstance().AddPost(post);
1848             }
1849         }
1850
1851         public void GetListStatus(bool read,
1852                                 TabClass tab,
1853                                 bool more,
1854                                 bool startup)
1855         {
1856             HttpStatusCode res;
1857             var content = "";
1858             var count = GetApiResultCount(MyCommon.WORKERTYPE.List, more, startup);
1859
1860             try
1861             {
1862                 if (more)
1863                 {
1864                     res = twCon.GetListsStatuses(tab.ListInfo.UserId, tab.ListInfo.Id, count, tab.OldestId, null, SettingCommon.Instance.IsListsIncludeRts, ref content);
1865                 }
1866                 else
1867                 {
1868                     res = twCon.GetListsStatuses(tab.ListInfo.UserId, tab.ListInfo.Id, count, null, null, SettingCommon.Instance.IsListsIncludeRts, ref content);
1869                 }
1870             }
1871             catch(Exception ex)
1872             {
1873                 throw new WebApiException("Err:" + ex.Message, ex);
1874             }
1875
1876             this.CheckStatusCode(res, content);
1877
1878             var minimumId = CreatePostsFromJson(content, MyCommon.WORKERTYPE.List, tab, read);
1879
1880             if (minimumId != null)
1881                 tab.OldestId = minimumId.Value;
1882         }
1883
1884         /// <summary>
1885         /// startStatusId からリプライ先の発言を辿る。発言は posts 以外からは検索しない。
1886         /// </summary>
1887         /// <returns>posts の中から検索されたリプライチェインの末端</returns>
1888         internal static PostClass FindTopOfReplyChain(IDictionary<Int64, PostClass> posts, Int64 startStatusId)
1889         {
1890             if (!posts.ContainsKey(startStatusId))
1891                 throw new ArgumentException("startStatusId (" + startStatusId + ") が posts の中から見つかりませんでした。", nameof(startStatusId));
1892
1893             var nextPost = posts[startStatusId];
1894             while (nextPost.InReplyToStatusId != null)
1895             {
1896                 if (!posts.ContainsKey(nextPost.InReplyToStatusId.Value))
1897                     break;
1898                 nextPost = posts[nextPost.InReplyToStatusId.Value];
1899             }
1900
1901             return nextPost;
1902         }
1903
1904         public void GetRelatedResult(bool read, TabClass tab)
1905         {
1906             var relPosts = new Dictionary<Int64, PostClass>();
1907             if (tab.RelationTargetPost.TextFromApi.Contains("@") && tab.RelationTargetPost.InReplyToStatusId == null)
1908             {
1909                 //検索結果対応
1910                 var p = TabInformations.GetInstance()[tab.RelationTargetPost.StatusId];
1911                 if (p != null && p.InReplyToStatusId != null)
1912                 {
1913                     tab.RelationTargetPost = p;
1914                 }
1915                 else
1916                 {
1917                     p = this.GetStatusApi(read, tab.RelationTargetPost.StatusId);
1918                     tab.RelationTargetPost = p;
1919                 }
1920             }
1921             relPosts.Add(tab.RelationTargetPost.StatusId, tab.RelationTargetPost);
1922
1923             Exception lastException = null;
1924
1925             // in_reply_to_status_id を使用してリプライチェインを辿る
1926             var nextPost = FindTopOfReplyChain(relPosts, tab.RelationTargetPost.StatusId);
1927             var loopCount = 1;
1928             while (nextPost.InReplyToStatusId != null && loopCount++ <= 20)
1929             {
1930                 var inReplyToId = nextPost.InReplyToStatusId.Value;
1931
1932                 var inReplyToPost = TabInformations.GetInstance()[inReplyToId];
1933                 if (inReplyToPost == null)
1934                 {
1935                     try
1936                     {
1937                         inReplyToPost = this.GetStatusApi(read, inReplyToId);
1938                     }
1939                     catch (WebApiException ex)
1940                     {
1941                         lastException = ex;
1942                         break;
1943                     }
1944                 }
1945
1946                 relPosts.Add(inReplyToPost.StatusId, inReplyToPost);
1947
1948                 nextPost = FindTopOfReplyChain(relPosts, nextPost.StatusId);
1949             }
1950
1951             //MRTとかに対応のためツイート内にあるツイートを指すURLを取り込む
1952             var text = tab.RelationTargetPost.Text;
1953             var ma = Twitter.StatusUrlRegex.Matches(text).Cast<Match>()
1954                 .Concat(Twitter.ThirdPartyStatusUrlRegex.Matches(text).Cast<Match>());
1955             foreach (var _match in ma)
1956             {
1957                 Int64 _statusId;
1958                 if (Int64.TryParse(_match.Groups["StatusId"].Value, out _statusId))
1959                 {
1960                     if (relPosts.ContainsKey(_statusId))
1961                         continue;
1962
1963                     var p = TabInformations.GetInstance()[_statusId];
1964                     if (p == null)
1965                     {
1966                         try
1967                         {
1968                             p = this.GetStatusApi(read, _statusId);
1969                         }
1970                         catch (WebApiException ex)
1971                         {
1972                             lastException = ex;
1973                             break;
1974                         }
1975                     }
1976
1977                     if (p != null)
1978                         relPosts.Add(p.StatusId, p);
1979                 }
1980             }
1981
1982             relPosts.Values.ToList().ForEach(p =>
1983             {
1984                 if (p.IsMe && !read && this._readOwnPost)
1985                     p.IsRead = true;
1986                 else
1987                     p.IsRead = read;
1988
1989                 tab.AddPostToInnerStorage(p);
1990             });
1991
1992             if (lastException != null)
1993                 throw new WebApiException(lastException.Message, lastException);
1994         }
1995
1996         public void GetSearch(bool read,
1997                             TabClass tab,
1998                             bool more)
1999         {
2000             HttpStatusCode res;
2001             var content = "";
2002             var count = GetApiResultCount(MyCommon.WORKERTYPE.PublicSearch, more, false);
2003             long? maxId = null;
2004             long? sinceId = null;
2005             if (more)
2006             {
2007                 maxId = tab.OldestId - 1;
2008             }
2009             else
2010             {
2011                 sinceId = tab.SinceId;
2012             }
2013
2014             try
2015             {
2016                 // TODO:一時的に40>100件に 件数変更UI作成の必要あり
2017                 res = twCon.Search(tab.SearchWords, tab.SearchLang, count, maxId, sinceId, ref content);
2018             }
2019             catch(Exception ex)
2020             {
2021                 throw new WebApiException("Err:" + ex.Message, ex);
2022             }
2023             switch (res)
2024             {
2025                 case HttpStatusCode.BadRequest:
2026                     throw new WebApiException("Invalid query", content);
2027                 case HttpStatusCode.NotFound:
2028                     throw new WebApiException("Invalid query", content);
2029                 case HttpStatusCode.PaymentRequired: //API Documentには420と書いてあるが、該当コードがないので402にしてある
2030                     throw new WebApiException("Search API Limit?", content);
2031                 case HttpStatusCode.OK:
2032                     break;
2033                 default:
2034                     throw new WebApiException("Err:" + res.ToString() + "(" + MethodBase.GetCurrentMethod().Name + ")", content);
2035             }
2036
2037             if (!TabInformations.GetInstance().ContainsTab(tab))
2038                 return;
2039
2040             var minimumId =  this.CreatePostsFromSearchJson(content, tab, read, count, more);
2041
2042             if (minimumId != null)
2043                 tab.OldestId = minimumId.Value;
2044         }
2045
2046         private void CreateDirectMessagesFromJson(string content, MyCommon.WORKERTYPE gType, bool read)
2047         {
2048             TwitterDirectMessage[] item;
2049             try
2050             {
2051                 if (gType == MyCommon.WORKERTYPE.UserStream)
2052                 {
2053                     item = new[] { TwitterStreamEventDirectMessage.ParseJson(content).DirectMessage };
2054                 }
2055                 else
2056                 {
2057                     item = TwitterDirectMessage.ParseJsonArray(content);
2058                 }
2059             }
2060             catch(SerializationException ex)
2061             {
2062                 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
2063                 throw new WebApiException("Json Parse Error(DataContractJsonSerializer)", content, ex);
2064             }
2065             catch(Exception ex)
2066             {
2067                 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
2068                 throw new WebApiException("Invalid Json!", content, ex);
2069             }
2070
2071             foreach (var message in item)
2072             {
2073                 var post = new PostClass();
2074                 try
2075                 {
2076                     post.StatusId = message.Id;
2077                     if (gType != MyCommon.WORKERTYPE.UserStream)
2078                     {
2079                         if (gType == MyCommon.WORKERTYPE.DirectMessegeRcv)
2080                         {
2081                             if (minDirectmessage > post.StatusId) minDirectmessage = post.StatusId;
2082                         }
2083                         else
2084                         {
2085                             if (minDirectmessageSent > post.StatusId) minDirectmessageSent = post.StatusId;
2086                         }
2087                     }
2088
2089                     //二重取得回避
2090                     lock (LockObj)
2091                     {
2092                         if (TabInformations.GetInstance().GetTabByType(MyCommon.TabUsageType.DirectMessage).Contains(post.StatusId)) continue;
2093                     }
2094                     //sender_id
2095                     //recipient_id
2096                     post.CreatedAt = MyCommon.DateTimeParse(message.CreatedAt);
2097                     //本文
2098                     var textFromApi = message.Text;
2099                     //HTMLに整形
2100                     post.Text = CreateHtmlAnchor(textFromApi, post.ReplyToList, message.Entities, post.Media);
2101                     post.TextFromApi = this.ReplaceTextFromApi(textFromApi, message.Entities);
2102                     post.TextFromApi = WebUtility.HtmlDecode(post.TextFromApi);
2103                     post.TextFromApi = post.TextFromApi.Replace("<3", "\u2661");
2104                     post.IsFav = false;
2105
2106                     post.QuoteStatusIds = GetQuoteTweetStatusIds(message.Entities).Distinct().ToArray();
2107
2108                     post.ExpandedUrls = message.Entities.OfType<TwitterEntityUrl>()
2109                         .Select(x => new PostClass.ExpandedUrlInfo(x.Url, x.ExpandedUrl))
2110                         .ToArray();
2111
2112                     //以下、ユーザー情報
2113                     TwitterUser user;
2114                     if (gType == MyCommon.WORKERTYPE.UserStream)
2115                     {
2116                         if (twCon.AuthenticatedUsername.Equals(message.Recipient.ScreenName, StringComparison.CurrentCultureIgnoreCase))
2117                         {
2118                             user = message.Sender;
2119                             post.IsMe = false;
2120                             post.IsOwl = true;
2121                         }
2122                         else
2123                         {
2124                             user = message.Recipient;
2125                             post.IsMe = true;
2126                             post.IsOwl = false;
2127                         }
2128                     }
2129                     else
2130                     {
2131                         if (gType == MyCommon.WORKERTYPE.DirectMessegeRcv)
2132                         {
2133                             user = message.Sender;
2134                             post.IsMe = false;
2135                             post.IsOwl = true;
2136                         }
2137                         else
2138                         {
2139                             user = message.Recipient;
2140                             post.IsMe = true;
2141                             post.IsOwl = false;
2142                         }
2143                     }
2144
2145                     post.UserId = user.Id;
2146                     post.ScreenName = user.ScreenName;
2147                     post.Nickname = user.Name.Trim();
2148                     post.ImageUrl = user.ProfileImageUrlHttps;
2149                     post.IsProtect = user.Protected;
2150                 }
2151                 catch(Exception ex)
2152                 {
2153                     MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
2154                     MessageBox.Show("Parse Error(CreateDirectMessagesFromJson)");
2155                     continue;
2156                 }
2157
2158                 post.IsRead = read;
2159                 if (post.IsMe && !read && _readOwnPost) post.IsRead = true;
2160                 post.IsReply = false;
2161                 post.IsExcludeReply = false;
2162                 post.IsDm = true;
2163
2164                 var dmTab = TabInformations.GetInstance().GetTabByType(MyCommon.TabUsageType.DirectMessage);
2165                 dmTab.AddPostToInnerStorage(post);
2166             }
2167         }
2168
2169         public void GetDirectMessageApi(bool read,
2170                                 MyCommon.WORKERTYPE gType,
2171                                 bool more)
2172         {
2173             this.CheckAccountState();
2174             this.CheckAccessLevel(TwitterApiAccessLevel.ReadWriteAndDirectMessage);
2175
2176             HttpStatusCode res;
2177             var content = "";
2178             var count = GetApiResultCount(gType, more, false);
2179
2180             try
2181             {
2182                 if (gType == MyCommon.WORKERTYPE.DirectMessegeRcv)
2183                 {
2184                     if (more)
2185                     {
2186                         res = twCon.DirectMessages(count, minDirectmessage, null, ref content);
2187                     }
2188                     else
2189                     {
2190                         res = twCon.DirectMessages(count, null, null, ref content);
2191                     }
2192                 }
2193                 else
2194                 {
2195                     if (more)
2196                     {
2197                         res = twCon.DirectMessagesSent(count, minDirectmessageSent, null, ref content);
2198                     }
2199                     else
2200                     {
2201                         res = twCon.DirectMessagesSent(count, null, null, ref content);
2202                     }
2203                 }
2204             }
2205             catch(Exception ex)
2206             {
2207                 throw new WebApiException("Err:" + ex.Message, ex);
2208             }
2209
2210             this.CheckStatusCode(res, content);
2211
2212             CreateDirectMessagesFromJson(content, gType, read);
2213         }
2214
2215         public void GetFavoritesApi(bool read,
2216                             bool more)
2217         {
2218             this.CheckAccountState();
2219
2220             HttpStatusCode res;
2221             var content = "";
2222             var count = GetApiResultCount(MyCommon.WORKERTYPE.Favorites, more, false);
2223
2224             try
2225             {
2226                 res = twCon.Favorites(count, ref content);
2227             }
2228             catch(Exception ex)
2229             {
2230                 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
2231             }
2232
2233             this.CheckStatusCode(res, content);
2234
2235             CreateFavoritePostsFromJson(content, read);
2236         }
2237
2238         private string ReplaceTextFromApi(string text, TwitterEntities entities)
2239         {
2240             if (entities != null)
2241             {
2242                 if (entities.Urls != null)
2243                 {
2244                     foreach (var m in entities.Urls)
2245                     {
2246                         if (!string.IsNullOrEmpty(m.DisplayUrl)) text = text.Replace(m.Url, m.DisplayUrl);
2247                     }
2248                 }
2249                 if (entities.Media != null)
2250                 {
2251                     foreach (var m in entities.Media)
2252                     {
2253                         if (!string.IsNullOrEmpty(m.DisplayUrl)) text = text.Replace(m.Url, m.DisplayUrl);
2254                     }
2255                 }
2256             }
2257             return text;
2258         }
2259
2260         /// <summary>
2261         /// フォロワーIDを更新します
2262         /// </summary>
2263         /// <exception cref="WebApiException"/>
2264         public void RefreshFollowerIds()
2265         {
2266             if (MyCommon._endingFlag) return;
2267
2268             var cursor = -1L;
2269             var newFollowerIds = new HashSet<long>();
2270             do
2271             {
2272                 var ret = this.GetFollowerIdsApi(ref cursor);
2273                 newFollowerIds.UnionWith(ret.Ids);
2274                 cursor = ret.NextCursor;
2275             } while (cursor != 0);
2276
2277             this.followerId = newFollowerIds;
2278             TabInformations.GetInstance().RefreshOwl(this.followerId);
2279
2280             this._GetFollowerResult = true;
2281         }
2282
2283         public bool GetFollowersSuccess
2284         {
2285             get
2286             {
2287                 return _GetFollowerResult;
2288             }
2289         }
2290
2291         private TwitterIds GetFollowerIdsApi(ref long cursor)
2292         {
2293             this.CheckAccountState();
2294
2295             HttpStatusCode res;
2296             var content = "";
2297             try
2298             {
2299                 res = twCon.FollowerIds(cursor, ref content);
2300             }
2301             catch(Exception e)
2302             {
2303                 throw new WebApiException("Err:" + e.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", e);
2304             }
2305
2306             this.CheckStatusCode(res, content);
2307
2308             try
2309             {
2310                 var ret = TwitterIds.ParseJson(content);
2311
2312                 if (ret.Ids == null)
2313                 {
2314                     var ex = new WebApiException("Err: ret.id == null (GetFollowerIdsApi)", content);
2315                     MyCommon.ExceptionOut(ex);
2316                     throw ex;
2317                 }
2318
2319                 return ret;
2320             }
2321             catch(SerializationException e)
2322             {
2323                 var ex = new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, e);
2324                 MyCommon.TraceOut(ex);
2325                 throw ex;
2326             }
2327             catch(Exception e)
2328             {
2329                 var ex = new WebApiException("Err:Invalid Json!", content, e);
2330                 MyCommon.TraceOut(ex);
2331                 throw ex;
2332             }
2333         }
2334
2335         /// <summary>
2336         /// RT 非表示ユーザーを更新します
2337         /// </summary>
2338         /// <exception cref="WebApiException"/>
2339         public void RefreshNoRetweetIds()
2340         {
2341             if (MyCommon._endingFlag) return;
2342
2343             this.noRTId = this.NoRetweetIdsApi();
2344
2345             this._GetNoRetweetResult = true;
2346         }
2347
2348         private long[] NoRetweetIdsApi()
2349         {
2350             this.CheckAccountState();
2351
2352             HttpStatusCode res;
2353             var content = "";
2354             try
2355             {
2356                 res = twCon.NoRetweetIds(ref content);
2357             }
2358             catch(Exception e)
2359             {
2360                 throw new WebApiException("Err:" + e.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", e);
2361             }
2362
2363             this.CheckStatusCode(res, content);
2364
2365             try
2366             {
2367                 return MyCommon.CreateDataFromJson<long[]>(content);
2368             }
2369             catch(SerializationException e)
2370             {
2371                 var ex = new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, e);
2372                 MyCommon.TraceOut(ex);
2373                 throw ex;
2374             }
2375             catch(Exception e)
2376             {
2377                 var ex = new WebApiException("Err:Invalid Json!", content, e);
2378                 MyCommon.TraceOut(ex);
2379                 throw ex;
2380             }
2381         }
2382
2383         public bool GetNoRetweetSuccess
2384         {
2385             get
2386             {
2387                 return _GetNoRetweetResult;
2388             }
2389         }
2390
2391         /// <summary>
2392         /// t.co の文字列長などの設定情報を更新します
2393         /// </summary>
2394         /// <exception cref="WebApiException"/>
2395         public void RefreshConfiguration()
2396         {
2397             this.Configuration = this.ConfigurationApi();
2398         }
2399
2400         private TwitterConfiguration ConfigurationApi()
2401         {
2402             HttpStatusCode res;
2403             var content = "";
2404             try
2405             {
2406                 res = twCon.GetConfiguration(ref content);
2407             }
2408             catch(Exception e)
2409             {
2410                 throw new WebApiException("Err:" + e.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", e);
2411             }
2412
2413             this.CheckStatusCode(res, content);
2414
2415             try
2416             {
2417                 return TwitterConfiguration.ParseJson(content);
2418             }
2419             catch(SerializationException e)
2420             {
2421                 var ex = new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, e);
2422                 MyCommon.TraceOut(ex);
2423                 throw ex;
2424             }
2425             catch(Exception e)
2426             {
2427                 var ex = new WebApiException("Err:Invalid Json!", content, e);
2428                 MyCommon.TraceOut(ex);
2429                 throw ex;
2430             }
2431         }
2432
2433         public void GetListsApi()
2434         {
2435             this.CheckAccountState();
2436
2437             HttpStatusCode res;
2438             IEnumerable<ListElement> lists;
2439             var content = "";
2440
2441             try
2442             {
2443                 res = twCon.GetLists(this.Username, ref content);
2444             }
2445             catch (Exception ex)
2446             {
2447                 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
2448             }
2449
2450             this.CheckStatusCode(res, content);
2451
2452             try
2453             {
2454                 lists = TwitterList.ParseJsonArray(content)
2455                     .Select(x => new ListElement(x, this));
2456             }
2457             catch (SerializationException ex)
2458             {
2459                 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
2460                 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
2461             }
2462             catch (Exception ex)
2463             {
2464                 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
2465                 throw new WebApiException("Err:Invalid Json!", content, ex);
2466             }
2467
2468             try
2469             {
2470                 res = twCon.GetListsSubscriptions(this.Username, ref content);
2471             }
2472             catch (Exception ex)
2473             {
2474                 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
2475             }
2476
2477             this.CheckStatusCode(res, content);
2478
2479             try
2480             {
2481                 lists = lists.Concat(TwitterList.ParseJsonArray(content)
2482                     .Select(x => new ListElement(x, this)));
2483             }
2484             catch (SerializationException ex)
2485             {
2486                 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
2487                 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
2488             }
2489             catch (Exception ex)
2490             {
2491                 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
2492                 throw new WebApiException("Err:Invalid Json!", content, ex);
2493             }
2494
2495             TabInformations.GetInstance().SubscribableLists = lists.ToList();
2496         }
2497
2498         public void DeleteList(string list_id)
2499         {
2500             HttpStatusCode res;
2501             var content = "";
2502
2503             try
2504             {
2505                 res = twCon.DeleteListID(this.Username, list_id, ref content);
2506             }
2507             catch(Exception ex)
2508             {
2509                 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
2510             }
2511
2512             this.CheckStatusCode(res, content);
2513         }
2514
2515         public ListElement EditList(string list_id, string new_name, bool isPrivate, string description)
2516         {
2517             HttpStatusCode res;
2518             var content = "";
2519
2520             try
2521             {
2522                 res = twCon.UpdateListID(this.Username, list_id, new_name, isPrivate, description, ref content);
2523             }
2524             catch(Exception ex)
2525             {
2526                 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
2527             }
2528
2529             this.CheckStatusCode(res, content);
2530
2531             try
2532             {
2533                 var le = TwitterList.ParseJson(content);
2534                 return  new ListElement(le, this);
2535             }
2536             catch(SerializationException ex)
2537             {
2538                 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
2539                 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
2540             }
2541             catch(Exception ex)
2542             {
2543                 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
2544                 throw new WebApiException("Err:Invalid Json!", content, ex);
2545             }
2546         }
2547
2548         public long GetListMembers(string list_id, List<UserInfo> lists, long cursor)
2549         {
2550             this.CheckAccountState();
2551
2552             HttpStatusCode res;
2553             var content = "";
2554             try
2555             {
2556                 res = twCon.GetListMembers(this.Username, list_id, cursor, ref content);
2557             }
2558             catch(Exception ex)
2559             {
2560                 throw new WebApiException("Err:" + ex.Message);
2561             }
2562
2563             this.CheckStatusCode(res, content);
2564
2565             try
2566             {
2567                 var users = TwitterUsers.ParseJson(content);
2568                 Array.ForEach<TwitterUser>(
2569                     users.Users,
2570                     u => lists.Add(new UserInfo(u)));
2571
2572                 return users.NextCursor;
2573             }
2574             catch(SerializationException ex)
2575             {
2576                 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
2577                 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
2578             }
2579             catch(Exception ex)
2580             {
2581                 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
2582                 throw new WebApiException("Err:Invalid Json!", content, ex);
2583             }
2584         }
2585
2586         public void CreateListApi(string listName, bool isPrivate, string description)
2587         {
2588             this.CheckAccountState();
2589
2590             HttpStatusCode res;
2591             var content = "";
2592             try
2593             {
2594                 res = twCon.CreateLists(listName, isPrivate, description, ref content);
2595             }
2596             catch(Exception ex)
2597             {
2598                 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
2599             }
2600
2601             this.CheckStatusCode(res, content);
2602
2603             try
2604             {
2605                 var le = TwitterList.ParseJson(content);
2606                 TabInformations.GetInstance().SubscribableLists.Add(new ListElement(le, this));
2607             }
2608             catch(SerializationException ex)
2609             {
2610                 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
2611                 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
2612             }
2613             catch(Exception ex)
2614             {
2615                 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
2616                 throw new WebApiException("Err:Invalid Json!", content, ex);
2617             }
2618         }
2619
2620         public bool ContainsUserAtList(string listId, string user)
2621         {
2622             this.CheckAccountState();
2623
2624             HttpStatusCode res;
2625             var content = "";
2626
2627             try
2628             {
2629                 res = this.twCon.ShowListMember(listId, user, ref content);
2630             }
2631             catch(Exception ex)
2632             {
2633                 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
2634             }
2635
2636             if (res == HttpStatusCode.NotFound)
2637             {
2638                 return false;
2639             }
2640
2641             this.CheckStatusCode(res, content);
2642
2643             try
2644             {
2645                 TwitterUser.ParseJson(content);
2646                 return true;
2647             }
2648             catch(Exception)
2649             {
2650                 return false;
2651             }
2652         }
2653
2654         public void AddUserToList(string listId, string user)
2655         {
2656             HttpStatusCode res;
2657             var content = "";
2658
2659             try
2660             {
2661                 res = twCon.CreateListMembers(listId, user, ref content);
2662             }
2663             catch(Exception ex)
2664             {
2665                 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
2666             }
2667
2668             this.CheckStatusCode(res, content);
2669         }
2670
2671         public void RemoveUserToList(string listId, string user)
2672         {
2673             HttpStatusCode res;
2674             var content = "";
2675
2676             try
2677             {
2678                 res = twCon.DeleteListMembers(listId, user, ref content);
2679             }
2680             catch(Exception ex)
2681             {
2682                 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
2683             }
2684
2685             this.CheckStatusCode(res, content);
2686         }
2687
2688         public string CreateHtmlAnchor(string text, List<string> AtList, TwitterEntities entities, List<MediaInfo> media)
2689         {
2690             if (entities != null)
2691             {
2692                 if (entities.Hashtags != null)
2693                 {
2694                     lock (this.LockObj)
2695                     {
2696                         this._hashList.AddRange(entities.Hashtags.Select(x => "#" + x.Text));
2697                     }
2698                 }
2699                 if (entities.UserMentions != null)
2700                 {
2701                     foreach (var ent in entities.UserMentions)
2702                     {
2703                         var screenName = ent.ScreenName.ToLowerInvariant();
2704                         if (!AtList.Contains(screenName))
2705                             AtList.Add(screenName);
2706                     }
2707                 }
2708                 if (entities.Media != null)
2709                 {
2710                     if (media != null)
2711                     {
2712                         foreach (var ent in entities.Media)
2713                         {
2714                             if (!media.Any(x => x.Url == ent.MediaUrl))
2715                             {
2716                                 if (ent.VideoInfo != null &&
2717                                     ent.Type == "animated_gif" || ent.Type == "video")
2718                                 {
2719                                     //var videoUrl = ent.VideoInfo.Variants
2720                                     //    .Where(v => v.ContentType == "video/mp4")
2721                                     //    .OrderByDescending(v => v.Bitrate)
2722                                     //    .Select(v => v.Url).FirstOrDefault();
2723                                     media.Add(new MediaInfo(ent.MediaUrl, ent.ExpandedUrl));
2724                                 }
2725                                 else
2726                                     media.Add(new MediaInfo(ent.MediaUrl));
2727                             }
2728                         }
2729                     }
2730                 }
2731             }
2732
2733             // PostClass.ExpandedUrlInfo を使用して非同期に URL 展開を行うためここでは expanded_url を使用しない
2734             text = TweetFormatter.AutoLinkHtml(text, entities, keepTco: true);
2735
2736             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>");
2737             text = PreProcessUrl(text); //IDN置換
2738
2739             return text;
2740         }
2741
2742         private static readonly Uri SourceUriBase = new Uri("https://twitter.com/");
2743
2744         /// <summary>
2745         /// Twitter APIから得たHTML形式のsource文字列を分析し、source名とURLに分離します
2746         /// </summary>
2747         public static Tuple<string, Uri> ParseSource(string sourceHtml)
2748         {
2749             if (string.IsNullOrEmpty(sourceHtml))
2750                 return Tuple.Create<string, Uri>("", null);
2751
2752             string sourceText;
2753             Uri sourceUri;
2754
2755             // sourceHtmlの例: <a href="http://twitter.com" rel="nofollow">Twitter Web Client</a>
2756
2757             var match = Regex.Match(sourceHtml, "^<a href=\"(?<uri>.+?)\".*?>(?<text>.+)</a>$", RegexOptions.IgnoreCase);
2758             if (match.Success)
2759             {
2760                 sourceText = WebUtility.HtmlDecode(match.Groups["text"].Value);
2761                 try
2762                 {
2763                     var uriStr = WebUtility.HtmlDecode(match.Groups["uri"].Value);
2764                     sourceUri = new Uri(SourceUriBase, uriStr);
2765                 }
2766                 catch (UriFormatException)
2767                 {
2768                     sourceUri = null;
2769                 }
2770             }
2771             else
2772             {
2773                 sourceText = WebUtility.HtmlDecode(sourceHtml);
2774                 sourceUri = null;
2775             }
2776
2777             return Tuple.Create(sourceText, sourceUri);
2778         }
2779
2780         public TwitterApiStatus GetInfoApi()
2781         {
2782             if (Twitter.AccountState != MyCommon.ACCOUNT_STATE.Valid) return null;
2783
2784             if (MyCommon._endingFlag) return null;
2785
2786             HttpStatusCode res;
2787             var content = "";
2788             try
2789             {
2790                 res = twCon.RateLimitStatus(ref content);
2791             }
2792             catch (Exception)
2793             {
2794                 this.ResetApiStatus();
2795                 return null;
2796             }
2797
2798             this.CheckStatusCode(res, content);
2799
2800             try
2801             {
2802                 MyCommon.TwitterApiInfo.UpdateFromJson(content);
2803                 return MyCommon.TwitterApiInfo;
2804             }
2805             catch (Exception ex)
2806             {
2807                 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
2808                 MyCommon.TwitterApiInfo.Reset();
2809                 return null;
2810             }
2811         }
2812
2813         /// <summary>
2814         /// ブロック中のユーザーを更新します
2815         /// </summary>
2816         /// <exception cref="WebApiException"/>
2817         public void RefreshBlockIds()
2818         {
2819             if (MyCommon._endingFlag) return;
2820
2821             var cursor = -1L;
2822             var newBlockIds = new HashSet<long>();
2823             do
2824             {
2825                 var ret = this.GetBlockIdsApi(cursor);
2826                 newBlockIds.UnionWith(ret.Ids);
2827                 cursor = ret.NextCursor;
2828             } while (cursor != 0);
2829
2830             newBlockIds.Remove(this.UserId); // 元のソースにあったので一応残しておく
2831
2832             TabInformations.GetInstance().BlockIds = newBlockIds;
2833         }
2834
2835         public TwitterIds GetBlockIdsApi(long cursor)
2836         {
2837             this.CheckAccountState();
2838
2839             HttpStatusCode res;
2840             var content = "";
2841             try
2842             {
2843                 res = twCon.GetBlockUserIds(ref content, cursor);
2844             }
2845             catch(Exception e)
2846             {
2847                 throw new WebApiException("Err:" + e.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", e);
2848             }
2849
2850             this.CheckStatusCode(res, content);
2851
2852             try
2853             {
2854                 return TwitterIds.ParseJson(content);
2855             }
2856             catch(SerializationException e)
2857             {
2858                 var ex = new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, e);
2859                 MyCommon.TraceOut(ex);
2860                 throw ex;
2861             }
2862             catch(Exception e)
2863             {
2864                 var ex = new WebApiException("Err:Invalid Json!", content, e);
2865                 MyCommon.TraceOut(ex);
2866                 throw ex;
2867             }
2868         }
2869
2870         /// <summary>
2871         /// ミュート中のユーザーIDを更新します
2872         /// </summary>
2873         /// <exception cref="WebApiException"/>
2874         public async Task RefreshMuteUserIdsAsync()
2875         {
2876             if (MyCommon._endingFlag) return;
2877
2878             var ids = await TwitterIds.GetAllItemsAsync(this.GetMuteUserIdsApiAsync)
2879                 .ConfigureAwait(false);
2880
2881             TabInformations.GetInstance().MuteUserIds = new HashSet<long>(ids);
2882         }
2883
2884         public async Task<TwitterIds> GetMuteUserIdsApiAsync(long cursor)
2885         {
2886             var content = "";
2887
2888             try
2889             {
2890                 var res = await Task.Run(() => twCon.GetMuteUserIds(ref content, cursor))
2891                     .ConfigureAwait(false);
2892
2893                 this.CheckStatusCode(res, content);
2894
2895                 return TwitterIds.ParseJson(content);
2896             }
2897             catch (WebException ex)
2898             {
2899                 var ex2 = new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", content, ex);
2900                 MyCommon.TraceOut(ex2);
2901                 throw ex2;
2902             }
2903             catch (SerializationException ex)
2904             {
2905                 var ex2 = new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
2906                 MyCommon.TraceOut(ex2);
2907                 throw ex2;
2908             }
2909         }
2910
2911         public string[] GetHashList()
2912         {
2913             string[] hashArray;
2914             lock (LockObj)
2915             {
2916                 hashArray = _hashList.ToArray();
2917                 _hashList.Clear();
2918             }
2919             return hashArray;
2920         }
2921
2922         public string AccessToken
2923         {
2924             get
2925             {
2926                 return twCon.AccessToken;
2927             }
2928         }
2929
2930         public string AccessTokenSecret
2931         {
2932             get
2933             {
2934                 return twCon.AccessTokenSecret;
2935             }
2936         }
2937
2938         private void CheckAccountState()
2939         {
2940             if (Twitter.AccountState != MyCommon.ACCOUNT_STATE.Valid)
2941                 throw new WebApiException("Auth error. Check your account");
2942         }
2943
2944         private void CheckAccessLevel(TwitterApiAccessLevel accessLevelFlags)
2945         {
2946             if (!this.AccessLevel.HasFlag(accessLevelFlags))
2947                 throw new WebApiException("Auth Err:try to re-authorization.");
2948         }
2949
2950         private void CheckStatusCode(HttpStatusCode httpStatus, string responseText,
2951             [CallerMemberName] string callerMethodName = "")
2952         {
2953             if (httpStatus == HttpStatusCode.OK)
2954             {
2955                 Twitter.AccountState = MyCommon.ACCOUNT_STATE.Valid;
2956                 return;
2957             }
2958
2959             if (string.IsNullOrWhiteSpace(responseText))
2960             {
2961                 if (httpStatus == HttpStatusCode.Unauthorized)
2962                     Twitter.AccountState = MyCommon.ACCOUNT_STATE.Invalid;
2963
2964                 throw new WebApiException("Err:" + httpStatus + "(" + callerMethodName + ")");
2965             }
2966
2967             try
2968             {
2969                 var errors = TwitterError.ParseJson(responseText).Errors;
2970                 if (errors == null || !errors.Any())
2971                 {
2972                     throw new WebApiException("Err:" + httpStatus + "(" + callerMethodName + ")", responseText);
2973                 }
2974
2975                 foreach (var error in errors)
2976                 {
2977                     if (error.Code == TwitterErrorCode.InvalidToken ||
2978                         error.Code == TwitterErrorCode.SuspendedAccount)
2979                     {
2980                         Twitter.AccountState = MyCommon.ACCOUNT_STATE.Invalid;
2981                     }
2982                 }
2983
2984                 throw new WebApiException("Err:" + string.Join(",", errors.Select(x => x.ToString())) + "(" + callerMethodName + ")", responseText);
2985             }
2986             catch (SerializationException) { }
2987
2988             throw new WebApiException("Err:" + httpStatus + "(" + callerMethodName + ")", responseText);
2989         }
2990
2991         public int GetTextLengthRemain(string postText)
2992         {
2993             var matchDm = Twitter.DMSendTextRegex.Match(postText);
2994             if (matchDm.Success)
2995                 return this.GetTextLengthRemainInternal(matchDm.Groups["body"].Value, isDm: true);
2996
2997             return this.GetTextLengthRemainInternal(postText, isDm: false);
2998         }
2999
3000         private int GetTextLengthRemainInternal(string postText, bool isDm)
3001         {
3002             var textLength = 0;
3003
3004             var pos = 0;
3005             while (pos < postText.Length)
3006             {
3007                 textLength++;
3008
3009                 if (char.IsSurrogatePair(postText, pos))
3010                     pos += 2; // サロゲートペアの場合は2文字分進める
3011                 else
3012                     pos++;
3013             }
3014
3015             var urls = TweetExtractor.ExtractUrls(postText);
3016             foreach (var url in urls)
3017             {
3018                 var shortUrlLength = url.StartsWith("https://", StringComparison.OrdinalIgnoreCase)
3019                     ? this.Configuration.ShortUrlLengthHttps
3020                     : this.Configuration.ShortUrlLength;
3021
3022                 textLength += shortUrlLength - url.Length;
3023             }
3024
3025             if (isDm)
3026                 return this.Configuration.DmTextCharacterLimit - textLength;
3027             else
3028                 return 140 - textLength;
3029         }
3030
3031
3032 #region "UserStream"
3033         private string trackWord_ = "";
3034         public string TrackWord
3035         {
3036             get
3037             {
3038                 return trackWord_;
3039             }
3040             set
3041             {
3042                 trackWord_ = value;
3043             }
3044         }
3045         private bool allAtReply_ = false;
3046         public bool AllAtReply
3047         {
3048             get
3049             {
3050                 return allAtReply_;
3051             }
3052             set
3053             {
3054                 allAtReply_ = value;
3055             }
3056         }
3057
3058         public event EventHandler NewPostFromStream;
3059         public event EventHandler UserStreamStarted;
3060         public event EventHandler UserStreamStopped;
3061         public event EventHandler<PostDeletedEventArgs> PostDeleted;
3062         public event EventHandler<UserStreamEventReceivedEventArgs> UserStreamEventReceived;
3063         private DateTime _lastUserstreamDataReceived;
3064         private TwitterUserstream userStream;
3065
3066         public class FormattedEvent
3067         {
3068             public MyCommon.EVENTTYPE Eventtype { get; set; }
3069             public DateTime CreatedAt { get; set; }
3070             public string Event { get; set; }
3071             public string Username { get; set; }
3072             public string Target { get; set; }
3073             public Int64 Id { get; set; }
3074             public bool IsMe { get; set; }
3075         }
3076
3077         public List<FormattedEvent> storedEvent_ = new List<FormattedEvent>();
3078         public List<FormattedEvent> StoredEvent
3079         {
3080             get
3081             {
3082                 return storedEvent_;
3083             }
3084             set
3085             {
3086                 storedEvent_ = value;
3087             }
3088         }
3089
3090         private readonly IReadOnlyDictionary<string, MyCommon.EVENTTYPE> eventTable = new Dictionary<string, MyCommon.EVENTTYPE>
3091         {
3092             ["favorite"] = MyCommon.EVENTTYPE.Favorite,
3093             ["unfavorite"] = MyCommon.EVENTTYPE.Unfavorite,
3094             ["follow"] = MyCommon.EVENTTYPE.Follow,
3095             ["list_member_added"] = MyCommon.EVENTTYPE.ListMemberAdded,
3096             ["list_member_removed"] = MyCommon.EVENTTYPE.ListMemberRemoved,
3097             ["block"] = MyCommon.EVENTTYPE.Block,
3098             ["unblock"] = MyCommon.EVENTTYPE.Unblock,
3099             ["user_update"] = MyCommon.EVENTTYPE.UserUpdate,
3100             ["deleted"] = MyCommon.EVENTTYPE.Deleted,
3101             ["list_created"] = MyCommon.EVENTTYPE.ListCreated,
3102             ["list_destroyed"] = MyCommon.EVENTTYPE.ListDestroyed,
3103             ["list_updated"] = MyCommon.EVENTTYPE.ListUpdated,
3104             ["unfollow"] = MyCommon.EVENTTYPE.Unfollow,
3105             ["list_user_subscribed"] = MyCommon.EVENTTYPE.ListUserSubscribed,
3106             ["list_user_unsubscribed"] = MyCommon.EVENTTYPE.ListUserUnsubscribed,
3107             ["mute"] = MyCommon.EVENTTYPE.Mute,
3108             ["unmute"] = MyCommon.EVENTTYPE.Unmute,
3109             ["quoted_tweet"] = MyCommon.EVENTTYPE.QuotedTweet,
3110         };
3111
3112         public bool IsUserstreamDataReceived
3113         {
3114             get
3115             {
3116                 return DateTime.Now.Subtract(this._lastUserstreamDataReceived).TotalSeconds < 31;
3117             }
3118         }
3119
3120         private void userStream_StatusArrived(string line)
3121         {
3122             this._lastUserstreamDataReceived = DateTime.Now;
3123             if (string.IsNullOrEmpty(line)) return;
3124
3125             if (line.First() != '{' || line.Last() != '}')
3126             {
3127                 MyCommon.TraceOut("Invalid JSON (StatusArrived):" + Environment.NewLine + line);
3128                 return;
3129             }
3130
3131             var isDm = false;
3132
3133             try
3134             {
3135                 using (var jsonReader = JsonReaderWriterFactory.CreateJsonReader(Encoding.UTF8.GetBytes(line), XmlDictionaryReaderQuotas.Max))
3136                 {
3137                     var xElm = XElement.Load(jsonReader);
3138                     if (xElm.Element("friends") != null)
3139                     {
3140                         Debug.WriteLine("friends");
3141                         return;
3142                     }
3143                     else if (xElm.Element("delete") != null)
3144                     {
3145                         Debug.WriteLine("delete");
3146                         Int64 id;
3147                         XElement idElm;
3148                         if ((idElm = xElm.Element("delete").Element("direct_message")?.Element("id")) != null)
3149                         {
3150                             id = 0;
3151                             long.TryParse(idElm.Value, out id);
3152
3153                             this.PostDeleted?.Invoke(this, new PostDeletedEventArgs(id));
3154                         }
3155                         else if ((idElm = xElm.Element("delete").Element("status")?.Element("id")) != null)
3156                         {
3157                             id = 0;
3158                             long.TryParse(idElm.Value, out id);
3159
3160                             this.PostDeleted?.Invoke(this, new PostDeletedEventArgs(id));
3161                         }
3162                         else
3163                         {
3164                             MyCommon.TraceOut("delete:" + line);
3165                             return;
3166                         }
3167                         for (int i = this.StoredEvent.Count - 1; i >= 0; i--)
3168                         {
3169                             var sEvt = this.StoredEvent[i];
3170                             if (sEvt.Id == id && (sEvt.Event == "favorite" || sEvt.Event == "unfavorite"))
3171                             {
3172                                 this.StoredEvent.RemoveAt(i);
3173                             }
3174                         }
3175                         return;
3176                     }
3177                     else if (xElm.Element("limit") != null)
3178                     {
3179                         Debug.WriteLine(line);
3180                         return;
3181                     }
3182                     else if (xElm.Element("event") != null)
3183                     {
3184                         Debug.WriteLine("event: " + xElm.Element("event").Value);
3185                         CreateEventFromJson(line);
3186                         return;
3187                     }
3188                     else if (xElm.Element("direct_message") != null)
3189                     {
3190                         Debug.WriteLine("direct_message");
3191                         isDm = true;
3192                     }
3193                     else if (xElm.Element("retweeted_status") != null)
3194                     {
3195                         var sourceUserId = xElm.XPathSelectElement("/user/id_str").Value;
3196                         var targetUserId = xElm.XPathSelectElement("/retweeted_status/user/id_str").Value;
3197
3198                         // 自分に関係しないリツイートの場合は無視する
3199                         var selfUserId = this.UserId.ToString();
3200                         if (sourceUserId == selfUserId || targetUserId == selfUserId)
3201                         {
3202                             // 公式 RT をイベントとしても扱う
3203                             var evt = CreateEventFromRetweet(xElm);
3204                             if (evt != null)
3205                             {
3206                                 this.StoredEvent.Insert(0, evt);
3207
3208                                 this.UserStreamEventReceived?.Invoke(this, new UserStreamEventReceivedEventArgs(evt));
3209                             }
3210                         }
3211
3212                         // 従来通り公式 RT の表示も行うため return しない
3213                     }
3214                     else if (xElm.Element("scrub_geo") != null)
3215                     {
3216                         try
3217                         {
3218                             TabInformations.GetInstance().ScrubGeoReserve(long.Parse(xElm.Element("scrub_geo").Element("user_id").Value),
3219                                                                         long.Parse(xElm.Element("scrub_geo").Element("up_to_status_id").Value));
3220                         }
3221                         catch(Exception)
3222                         {
3223                             MyCommon.TraceOut("scrub_geo:" + line);
3224                         }
3225                         return;
3226                     }
3227                 }
3228
3229                 if (isDm)
3230                 {
3231                     CreateDirectMessagesFromJson(line, MyCommon.WORKERTYPE.UserStream, false);
3232                 }
3233                 else
3234                 {
3235                     CreatePostsFromJson("[" + line + "]", MyCommon.WORKERTYPE.Timeline, null, false);
3236                 }
3237             }
3238             catch (WebApiException ex)
3239             {
3240                 MyCommon.TraceOut(ex);
3241                 return;
3242             }
3243             catch(NullReferenceException)
3244             {
3245                 MyCommon.TraceOut("NullRef StatusArrived: " + line);
3246             }
3247
3248             this.NewPostFromStream?.Invoke(this, EventArgs.Empty);
3249         }
3250
3251         /// <summary>
3252         /// UserStreamsから受信した公式RTをイベントに変換します
3253         /// </summary>
3254         private FormattedEvent CreateEventFromRetweet(XElement xElm)
3255         {
3256             return new FormattedEvent
3257             {
3258                 Eventtype = MyCommon.EVENTTYPE.Retweet,
3259                 Event = "retweet",
3260                 CreatedAt = MyCommon.DateTimeParse(xElm.XPathSelectElement("/created_at").Value),
3261                 IsMe = xElm.XPathSelectElement("/user/id_str").Value == this.UserId.ToString(),
3262                 Username = xElm.XPathSelectElement("/user/screen_name").Value,
3263                 Target = string.Format("@{0}:{1}", new[]
3264                 {
3265                     xElm.XPathSelectElement("/retweeted_status/user/screen_name").Value,
3266                     WebUtility.HtmlDecode(xElm.XPathSelectElement("/retweeted_status/text").Value),
3267                 }),
3268                 Id = long.Parse(xElm.XPathSelectElement("/retweeted_status/id_str").Value),
3269             };
3270         }
3271
3272         private void CreateEventFromJson(string content)
3273         {
3274             TwitterStreamEvent eventData = null;
3275             try
3276             {
3277                 eventData = TwitterStreamEvent.ParseJson(content);
3278             }
3279             catch(SerializationException ex)
3280             {
3281                 MyCommon.TraceOut(ex, "Event Serialize Exception!" + Environment.NewLine + content);
3282             }
3283             catch(Exception ex)
3284             {
3285                 MyCommon.TraceOut(ex, "Event Exception!" + Environment.NewLine + content);
3286             }
3287
3288             var evt = new FormattedEvent();
3289             evt.CreatedAt = MyCommon.DateTimeParse(eventData.CreatedAt);
3290             evt.Event = eventData.Event;
3291             evt.Username = eventData.Source.ScreenName;
3292             evt.IsMe = evt.Username.ToLowerInvariant().Equals(this.Username.ToLowerInvariant());
3293
3294             MyCommon.EVENTTYPE eventType;
3295             eventTable.TryGetValue(eventData.Event, out eventType);
3296             evt.Eventtype = eventType;
3297
3298             TwitterStreamEvent<TwitterStatus> tweetEvent;
3299
3300             switch (eventData.Event)
3301             {
3302                 case "access_revoked":
3303                 case "access_unrevoked":
3304                 case "user_delete":
3305                 case "user_suspend":
3306                     return;
3307                 case "follow":
3308                     if (eventData.Target.ScreenName.ToLowerInvariant().Equals(_uname))
3309                     {
3310                         if (!this.followerId.Contains(eventData.Source.Id)) this.followerId.Add(eventData.Source.Id);
3311                     }
3312                     else
3313                     {
3314                         return;    //Block後のUndoをすると、SourceとTargetが逆転したfollowイベントが帰ってくるため。
3315                     }
3316                     evt.Target = "";
3317                     break;
3318                 case "unfollow":
3319                     evt.Target = "@" + eventData.Target.ScreenName;
3320                     break;
3321                 case "favorited_retweet":
3322                 case "retweeted_retweet":
3323                     return;
3324                 case "favorite":
3325                 case "unfavorite":
3326                     tweetEvent = TwitterStreamEvent<TwitterStatus>.ParseJson(content);
3327                     evt.Target = "@" + tweetEvent.TargetObject.User.ScreenName + ":" + WebUtility.HtmlDecode(tweetEvent.TargetObject.Text);
3328                     evt.Id = tweetEvent.TargetObject.Id;
3329
3330                     if (SettingCommon.Instance.IsRemoveSameEvent)
3331                     {
3332                         if (this.StoredEvent.Any(ev => ev.Username == evt.Username && ev.Eventtype == evt.Eventtype && ev.Target == evt.Target))
3333                             return;
3334                     }
3335
3336                     var tabinfo = TabInformations.GetInstance();
3337
3338                     PostClass post;
3339                     var statusId = tweetEvent.TargetObject.Id;
3340                     if (!tabinfo.Posts.TryGetValue(statusId, out post))
3341                         break;
3342
3343                     if (eventData.Event == "favorite")
3344                     {
3345                         var favTab = tabinfo.GetTabByType(MyCommon.TabUsageType.Favorites);
3346                         if (!favTab.Contains(post.StatusId))
3347                             favTab.AddPostImmediately(post.StatusId, post.IsRead);
3348
3349                         if (tweetEvent.Source.Id == this.UserId)
3350                         {
3351                             post.IsFav = true;
3352                         }
3353                         else if (tweetEvent.Target.Id == this.UserId)
3354                         {
3355                             post.FavoritedCount++;
3356
3357                             if (SettingCommon.Instance.FavEventUnread)
3358                                 tabinfo.SetReadAllTab(post.StatusId, read: false);
3359                         }
3360                     }
3361                     else // unfavorite
3362                     {
3363                         if (tweetEvent.Source.Id == this.UserId)
3364                         {
3365                             post.IsFav = false;
3366                         }
3367                         else if (tweetEvent.Target.Id == this.UserId)
3368                         {
3369                             post.FavoritedCount = Math.Max(0, post.FavoritedCount - 1);
3370                         }
3371                     }
3372                     break;
3373                 case "quoted_tweet":
3374                     if (evt.IsMe) return;
3375
3376                     tweetEvent = TwitterStreamEvent<TwitterStatus>.ParseJson(content);
3377                     evt.Target = "@" + tweetEvent.TargetObject.User.ScreenName + ":" + WebUtility.HtmlDecode(tweetEvent.TargetObject.Text);
3378                     evt.Id = tweetEvent.TargetObject.Id;
3379
3380                     if (SettingCommon.Instance.IsRemoveSameEvent)
3381                     {
3382                         if (this.StoredEvent.Any(ev => ev.Username == evt.Username && ev.Eventtype == evt.Eventtype && ev.Target == evt.Target))
3383                             return;
3384                     }
3385                     break;
3386                 case "list_member_added":
3387                 case "list_member_removed":
3388                 case "list_created":
3389                 case "list_destroyed":
3390                 case "list_updated":
3391                 case "list_user_subscribed":
3392                 case "list_user_unsubscribed":
3393                     var listEvent = TwitterStreamEvent<TwitterList>.ParseJson(content);
3394                     evt.Target = listEvent.TargetObject.FullName;
3395                     break;
3396                 case "block":
3397                     if (!TabInformations.GetInstance().BlockIds.Contains(eventData.Target.Id)) TabInformations.GetInstance().BlockIds.Add(eventData.Target.Id);
3398                     evt.Target = "";
3399                     break;
3400                 case "unblock":
3401                     if (TabInformations.GetInstance().BlockIds.Contains(eventData.Target.Id)) TabInformations.GetInstance().BlockIds.Remove(eventData.Target.Id);
3402                     evt.Target = "";
3403                     break;
3404                 case "user_update":
3405                     evt.Target = "";
3406                     break;
3407                 
3408                 // Mute / Unmute
3409                 case "mute":
3410                     evt.Target = "@" + eventData.Target.ScreenName;
3411                     if (!TabInformations.GetInstance().MuteUserIds.Contains(eventData.Target.Id))
3412                     {
3413                         TabInformations.GetInstance().MuteUserIds.Add(eventData.Target.Id);
3414                     }
3415                     break;
3416                 case "unmute":
3417                     evt.Target = "@" + eventData.Target.ScreenName;
3418                     if (TabInformations.GetInstance().MuteUserIds.Contains(eventData.Target.Id))
3419                     {
3420                         TabInformations.GetInstance().MuteUserIds.Remove(eventData.Target.Id);
3421                     }
3422                     break;
3423
3424                 default:
3425                     MyCommon.TraceOut("Unknown Event:" + evt.Event + Environment.NewLine + content);
3426                     break;
3427             }
3428             this.StoredEvent.Insert(0, evt);
3429
3430             this.UserStreamEventReceived?.Invoke(this, new UserStreamEventReceivedEventArgs(evt));
3431         }
3432
3433         private void userStream_Started()
3434         {
3435             this.UserStreamStarted?.Invoke(this, EventArgs.Empty);
3436         }
3437
3438         private void userStream_Stopped()
3439         {
3440             this.UserStreamStopped?.Invoke(this, EventArgs.Empty);
3441         }
3442
3443         public bool UserStreamEnabled
3444         {
3445             get
3446             {
3447                 return userStream == null ? false : userStream.Enabled;
3448             }
3449         }
3450
3451         public void StartUserStream()
3452         {
3453             if (userStream != null)
3454             {
3455                 StopUserStream();
3456             }
3457             userStream = new TwitterUserstream(twCon);
3458             userStream.StatusArrived += userStream_StatusArrived;
3459             userStream.Started += userStream_Started;
3460             userStream.Stopped += userStream_Stopped;
3461             userStream.Start(this.AllAtReply, this.TrackWord);
3462         }
3463
3464         public void StopUserStream()
3465         {
3466             userStream?.Dispose();
3467             userStream = null;
3468             if (!MyCommon._endingFlag)
3469             {
3470                 this.UserStreamStopped?.Invoke(this, EventArgs.Empty);
3471             }
3472         }
3473
3474         public void ReconnectUserStream()
3475         {
3476             if (userStream != null)
3477             {
3478                 this.StartUserStream();
3479             }
3480         }
3481
3482         private class TwitterUserstream : IDisposable
3483         {
3484             public event Action<string> StatusArrived;
3485             public event Action Stopped;
3486             public event Action Started;
3487             private HttpTwitter twCon;
3488
3489             private Thread _streamThread;
3490             private bool _streamActive;
3491
3492             private bool _allAtreplies = false;
3493             private string _trackwords = "";
3494
3495             public TwitterUserstream(HttpTwitter twitterConnection)
3496             {
3497                 twCon = (HttpTwitter)twitterConnection.Clone();
3498             }
3499
3500             public void Start(bool allAtReplies, string trackwords)
3501             {
3502                 this.AllAtReplies = allAtReplies;
3503                 this.TrackWords = trackwords;
3504                 _streamActive = true;
3505                 if (_streamThread != null && _streamThread.IsAlive) return;
3506                 _streamThread = new Thread(UserStreamLoop);
3507                 _streamThread.Name = "UserStreamReceiver";
3508                 _streamThread.IsBackground = true;
3509                 _streamThread.Start();
3510             }
3511
3512             public bool Enabled
3513             {
3514                 get
3515                 {
3516                     return _streamActive;
3517                 }
3518             }
3519
3520             public bool AllAtReplies
3521             {
3522                 get
3523                 {
3524                     return _allAtreplies;
3525                 }
3526                 set
3527                 {
3528                     _allAtreplies = value;
3529                 }
3530             }
3531
3532             public string TrackWords
3533             {
3534                 get
3535                 {
3536                     return _trackwords;
3537                 }
3538                 set
3539                 {
3540                     _trackwords = value;
3541                 }
3542             }
3543
3544             private void UserStreamLoop()
3545             {
3546                 var sleepSec = 0;
3547                 do
3548                 {
3549                     Stream st = null;
3550                     StreamReader sr = null;
3551                     try
3552                     {
3553                         if (!MyCommon.IsNetworkAvailable())
3554                         {
3555                             sleepSec = 30;
3556                             continue;
3557                         }
3558
3559                         Started?.Invoke();
3560
3561                         var res = twCon.UserStream(ref st, _allAtreplies, _trackwords, Networking.GetUserAgentString());
3562
3563                         switch (res)
3564                         {
3565                             case HttpStatusCode.OK:
3566                                 Twitter.AccountState = MyCommon.ACCOUNT_STATE.Valid;
3567                                 break;
3568                             case HttpStatusCode.Unauthorized:
3569                                 Twitter.AccountState = MyCommon.ACCOUNT_STATE.Invalid;
3570                                 sleepSec = 120;
3571                                 continue;
3572                         }
3573
3574                         if (st == null)
3575                         {
3576                             sleepSec = 30;
3577                             //MyCommon.TraceOut("Stop:stream is null")
3578                             continue;
3579                         }
3580
3581                         sr = new StreamReader(st);
3582
3583                         while (_streamActive && !sr.EndOfStream && Twitter.AccountState == MyCommon.ACCOUNT_STATE.Valid)
3584                         {
3585                             StatusArrived?.Invoke(sr.ReadLine());
3586                             //this.LastTime = Now;
3587                         }
3588
3589                         if (sr.EndOfStream || Twitter.AccountState == MyCommon.ACCOUNT_STATE.Invalid)
3590                         {
3591                             sleepSec = 30;
3592                             //MyCommon.TraceOut("Stop:EndOfStream")
3593                             continue;
3594                         }
3595                         break;
3596                     }
3597                     catch(WebException ex)
3598                     {
3599                         if (ex.Status == WebExceptionStatus.Timeout)
3600                         {
3601                             sleepSec = 30;                        //MyCommon.TraceOut("Stop:Timeout")
3602                         }
3603                         else if (ex.Response != null && (int)((HttpWebResponse)ex.Response).StatusCode == 420)
3604                         {
3605                             //MyCommon.TraceOut("Stop:Connection Limit")
3606                             break;
3607                         }
3608                         else
3609                         {
3610                             sleepSec = 30;
3611                             //MyCommon.TraceOut("Stop:WebException " + ex.Status.ToString())
3612                         }
3613                     }
3614                     catch(ThreadAbortException)
3615                     {
3616                         break;
3617                     }
3618                     catch(IOException)
3619                     {
3620                         sleepSec = 30;
3621                         //MyCommon.TraceOut("Stop:IOException with Active." + Environment.NewLine + ex.Message)
3622                     }
3623                     catch(ArgumentException ex)
3624                     {
3625                         //System.ArgumentException: ストリームを読み取れませんでした。
3626                         //サーバー側もしくは通信経路上で切断された場合?タイムアウト頻発後発生
3627                         sleepSec = 30;
3628                         MyCommon.TraceOut(ex, "Stop:ArgumentException");
3629                     }
3630                     catch(Exception ex)
3631                     {
3632                         MyCommon.TraceOut("Stop:Exception." + Environment.NewLine + ex.Message);
3633                         MyCommon.ExceptionOut(ex);
3634                         sleepSec = 30;
3635                     }
3636                     finally
3637                     {
3638                         if (_streamActive)
3639                         {
3640                             Stopped?.Invoke();
3641                         }
3642                         twCon.RequestAbort();
3643                         sr?.Close();
3644                         if (sleepSec > 0)
3645                         {
3646                             var ms = 0;
3647                             while (_streamActive && ms < sleepSec * 1000)
3648                             {
3649                                 Thread.Sleep(500);
3650                                 ms += 500;
3651                             }
3652                         }
3653                         sleepSec = 0;
3654                     }
3655                 } while (this._streamActive);
3656
3657                 if (_streamActive)
3658                 {
3659                     Stopped?.Invoke();
3660                 }
3661                 MyCommon.TraceOut("Stop:EndLoop");
3662             }
3663
3664 #region "IDisposable Support"
3665             private bool disposedValue; // 重複する呼び出しを検出するには
3666
3667             // IDisposable
3668             protected virtual void Dispose(bool disposing)
3669             {
3670                 if (!this.disposedValue)
3671                 {
3672                     if (disposing)
3673                     {
3674                         _streamActive = false;
3675                         if (_streamThread != null && _streamThread.IsAlive)
3676                         {
3677                             _streamThread.Abort();
3678                         }
3679                     }
3680                 }
3681                 this.disposedValue = true;
3682             }
3683
3684             //protected Overrides void Finalize()
3685             //{
3686             //    // このコードを変更しないでください。クリーンアップ コードを上の Dispose(bool disposing) に記述します。
3687             //    Dispose(false)
3688             //    MyBase.Finalize()
3689             //}
3690
3691             // このコードは、破棄可能なパターンを正しく実装できるように Visual Basic によって追加されました。
3692             public void Dispose()
3693             {
3694                 // このコードを変更しないでください。クリーンアップ コードを上の Dispose(bool disposing) に記述します。
3695                 Dispose(true);
3696                 GC.SuppressFinalize(this);
3697             }
3698 #endregion
3699
3700         }
3701 #endregion
3702
3703 #region "IDisposable Support"
3704         private bool disposedValue; // 重複する呼び出しを検出するには
3705
3706         // IDisposable
3707         protected virtual void Dispose(bool disposing)
3708         {
3709             if (!this.disposedValue)
3710             {
3711                 if (disposing)
3712                 {
3713                     this.StopUserStream();
3714                 }
3715             }
3716             this.disposedValue = true;
3717         }
3718
3719         //protected Overrides void Finalize()
3720         //{
3721         //    // このコードを変更しないでください。クリーンアップ コードを上の Dispose(bool disposing) に記述します。
3722         //    Dispose(false)
3723         //    MyBase.Finalize()
3724         //}
3725
3726         // このコードは、破棄可能なパターンを正しく実装できるように Visual Basic によって追加されました。
3727         public void Dispose()
3728         {
3729             // このコードを変更しないでください。クリーンアップ コードを上の Dispose(bool disposing) に記述します。
3730             Dispose(true);
3731             GC.SuppressFinalize(this);
3732         }
3733 #endregion
3734     }
3735
3736     public class PostDeletedEventArgs : EventArgs
3737     {
3738         public long StatusId { get; }
3739
3740         public PostDeletedEventArgs(long statusId)
3741         {
3742             this.StatusId = statusId;
3743         }
3744     }
3745
3746     public class UserStreamEventReceivedEventArgs : EventArgs
3747     {
3748         public Twitter.FormattedEvent EventData { get; }
3749
3750         public UserStreamEventReceivedEventArgs(Twitter.FormattedEvent eventData)
3751         {
3752             this.EventData = eventData;
3753         }
3754     }
3755 }