OSDN Git Service

ba8403bc71fd2de2109500a48fe0b8bed118abea
[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 _restrictFavCheck;
158
159         private bool _readOwnPost;
160         private List<string> _hashList = new List<string>();
161
162         //max_idで古い発言を取得するために保持(lists分は個別タブで管理)
163         private long minHomeTimeline = long.MaxValue;
164         private long minMentions = long.MaxValue;
165         private long minDirectmessage = long.MaxValue;
166         private long minDirectmessageSent = long.MaxValue;
167
168         //private FavoriteQueue favQueue;
169
170         private HttpTwitter twCon = new HttpTwitter();
171
172         //private List<PostClass> _deletemessages = new List<PostClass>();
173
174         public Twitter()
175         {
176             this.Configuration = TwitterConfiguration.DefaultConfiguration();
177         }
178
179         public TwitterApiAccessLevel AccessLevel
180         {
181             get
182             {
183                 return MyCommon.TwitterApiInfo.AccessLevel;
184             }
185         }
186
187         protected void ResetApiStatus()
188         {
189             MyCommon.TwitterApiInfo.Reset();
190         }
191
192         public void Authenticate(string username, string password)
193         {
194             this.ResetApiStatus();
195
196             HttpStatusCode res;
197             var content = "";
198             try
199             {
200                 res = twCon.AuthUserAndPass(username, password, ref content);
201             }
202             catch(Exception ex)
203             {
204                 throw new WebApiException("Err:" + ex.Message, ex);
205             }
206
207             this.CheckStatusCode(res, content);
208
209             _uname = username.ToLower();
210             if (SettingCommon.Instance.UserstreamStartup) this.ReconnectUserStream();
211         }
212
213         public string StartAuthentication()
214         {
215             //OAuth PIN Flow
216             this.ResetApiStatus();
217             try
218             {
219                 string pinPageUrl = null;
220                 var res = twCon.AuthGetRequestToken(ref pinPageUrl);
221                 if (!res)
222                     throw new WebApiException("Err:Failed to access auth server.");
223
224                 return pinPageUrl;
225             }
226             catch (Exception ex)
227             {
228                 throw new WebApiException("Err:Failed to access auth server.", ex);
229             }
230         }
231
232         public void Authenticate(string pinCode)
233         {
234             this.ResetApiStatus();
235
236             HttpStatusCode res;
237             try
238             {
239                 res = twCon.AuthGetAccessToken(pinCode);
240             }
241             catch (Exception ex)
242             {
243                 throw new WebApiException("Err:Failed to access auth acc server.", ex);
244             }
245
246             this.CheckStatusCode(res, null);
247
248             _uname = Username.ToLower();
249             if (SettingCommon.Instance.UserstreamStartup) this.ReconnectUserStream();
250         }
251
252         public void ClearAuthInfo()
253         {
254             Twitter.AccountState = MyCommon.ACCOUNT_STATE.Invalid;
255             this.ResetApiStatus();
256             twCon.ClearAuthInfo();
257         }
258
259         public void VerifyCredentials()
260         {
261             HttpStatusCode res;
262             var content = "";
263             try
264             {
265                 res = twCon.VerifyCredentials(ref content);
266             }
267             catch (Exception ex)
268             {
269                 throw new WebApiException("Err:" + ex.Message, ex);
270             }
271
272             this.CheckStatusCode(res, content);
273
274             try
275             {
276                 var user = TwitterUser.ParseJson(content);
277
278                 this.twCon.AuthenticatedUserId = user.Id;
279                 this.UpdateUserStats(user);
280             }
281             catch (SerializationException ex)
282             {
283                 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
284                 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
285             }
286         }
287
288         public void Initialize(string token, string tokenSecret, string username, long userId)
289         {
290             //OAuth認証
291             if (string.IsNullOrEmpty(token) || string.IsNullOrEmpty(tokenSecret) || string.IsNullOrEmpty(username))
292             {
293                 Twitter.AccountState = MyCommon.ACCOUNT_STATE.Invalid;
294             }
295             this.ResetApiStatus();
296             twCon.Initialize(token, tokenSecret, username, userId);
297             _uname = username.ToLower();
298             if (SettingCommon.Instance.UserstreamStartup) this.ReconnectUserStream();
299         }
300
301         public string PreProcessUrl(string orgData)
302         {
303             int posl1;
304             var posl2 = 0;
305             //var IDNConveter = new IdnMapping();
306             var href = "<a href=\"";
307
308             while (true)
309             {
310                 if (orgData.IndexOf(href, posl2, StringComparison.Ordinal) > -1)
311                 {
312                     var urlStr = "";
313                     // IDN展開
314                     posl1 = orgData.IndexOf(href, posl2, StringComparison.Ordinal);
315                     posl1 += href.Length;
316                     posl2 = orgData.IndexOf("\"", posl1, StringComparison.Ordinal);
317                     urlStr = orgData.Substring(posl1, posl2 - posl1);
318
319                     if (!urlStr.StartsWith("http://") && !urlStr.StartsWith("https://") && !urlStr.StartsWith("ftp://"))
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
1133         {
1134             set
1135             {
1136                 _restrictFavCheck = value;
1137             }
1138         }
1139
1140 #region "バージョンアップ"
1141         public void GetTweenBinary(string strVer)
1142         {
1143             try
1144             {
1145                 //本体
1146                 if (!(new HttpVarious()).GetDataToFile("http://tween.sourceforge.jp/Tween" + strVer + ".gz?" + DateTime.Now.ToString("yyMMddHHmmss") + Environment.TickCount.ToString(),
1147                                                     Path.Combine(MyCommon.settingPath, "TweenNew.exe")))
1148                 {
1149                     throw new WebApiException("Err:Download failed");
1150                 }
1151                 //英語リソース
1152                 if (!Directory.Exists(Path.Combine(MyCommon.settingPath, "en")))
1153                 {
1154                     Directory.CreateDirectory(Path.Combine(MyCommon.settingPath, "en"));
1155                 }
1156                 if (!(new HttpVarious()).GetDataToFile("http://tween.sourceforge.jp/TweenResEn" + strVer + ".gz?" + DateTime.Now.ToString("yyMMddHHmmss") + Environment.TickCount.ToString(),
1157                                                     Path.Combine(Path.Combine(MyCommon.settingPath, "en"), "Tween.resourcesNew.dll")))
1158                 {
1159                     throw new WebApiException("Err:Download failed");
1160                 }
1161                 //その他言語圏のリソース。取得失敗しても継続
1162                 //UIの言語圏のリソース
1163                 var curCul = "";
1164                 if (!Thread.CurrentThread.CurrentUICulture.IsNeutralCulture)
1165                 {
1166                     var idx = Thread.CurrentThread.CurrentUICulture.Name.LastIndexOf('-');
1167                     if (idx > -1)
1168                     {
1169                         curCul = Thread.CurrentThread.CurrentUICulture.Name.Substring(0, idx);
1170                     }
1171                     else
1172                     {
1173                         curCul = Thread.CurrentThread.CurrentUICulture.Name;
1174                     }
1175                 }
1176                 else
1177                 {
1178                     curCul = Thread.CurrentThread.CurrentUICulture.Name;
1179                 }
1180                 if (!string.IsNullOrEmpty(curCul) && curCul != "en" && curCul != "ja")
1181                 {
1182                     if (!Directory.Exists(Path.Combine(MyCommon.settingPath, curCul)))
1183                     {
1184                         Directory.CreateDirectory(Path.Combine(MyCommon.settingPath, curCul));
1185                     }
1186                     if (!(new HttpVarious()).GetDataToFile("http://tween.sourceforge.jp/TweenRes" + curCul + strVer + ".gz?" + DateTime.Now.ToString("yyMMddHHmmss") + Environment.TickCount.ToString(),
1187                                                         Path.Combine(Path.Combine(MyCommon.settingPath, curCul), "Tween.resourcesNew.dll")))
1188                     {
1189                         //return "Err:Download failed";
1190                     }
1191                 }
1192                 //スレッドの言語圏のリソース
1193                 string curCul2;
1194                 if (!Thread.CurrentThread.CurrentCulture.IsNeutralCulture)
1195                 {
1196                     var idx = Thread.CurrentThread.CurrentCulture.Name.LastIndexOf('-');
1197                     if (idx > -1)
1198                     {
1199                         curCul2 = Thread.CurrentThread.CurrentCulture.Name.Substring(0, idx);
1200                     }
1201                     else
1202                     {
1203                         curCul2 = Thread.CurrentThread.CurrentCulture.Name;
1204                     }
1205                 }
1206                 else
1207                 {
1208                     curCul2 = Thread.CurrentThread.CurrentCulture.Name;
1209                 }
1210                 if (!string.IsNullOrEmpty(curCul2) && curCul2 != "en" && curCul2 != curCul)
1211                 {
1212                     if (!Directory.Exists(Path.Combine(MyCommon.settingPath, curCul2)))
1213                     {
1214                         Directory.CreateDirectory(Path.Combine(MyCommon.settingPath, curCul2));
1215                     }
1216                     if (!(new HttpVarious()).GetDataToFile("http://tween.sourceforge.jp/TweenRes" + curCul2 + strVer + ".gz?" + DateTime.Now.ToString("yyMMddHHmmss") + Environment.TickCount.ToString(),
1217                                                     Path.Combine(Path.Combine(MyCommon.settingPath, curCul2), "Tween.resourcesNew.dll")))
1218                     {
1219                         //return "Err:Download failed";
1220                     }
1221                 }
1222
1223                 //アップデータ
1224                 if (!(new HttpVarious()).GetDataToFile("http://tween.sourceforge.jp/TweenUp3.gz?" + DateTime.Now.ToString("yyMMddHHmmss") + Environment.TickCount.ToString(),
1225                                                     Path.Combine(MyCommon.settingPath, "TweenUp3.exe")))
1226                 {
1227                     throw new WebApiException("Err:Download failed");
1228                 }
1229                 //シリアライザDLL
1230                 if (!(new HttpVarious()).GetDataToFile("http://tween.sourceforge.jp/TweenDll" + strVer + ".gz?" + DateTime.Now.ToString("yyMMddHHmmss") + Environment.TickCount.ToString(),
1231                                                     Path.Combine(MyCommon.settingPath, "TweenNew.XmlSerializers.dll")))
1232                 {
1233                     throw new WebApiException("Err:Download failed");
1234                 }
1235             }
1236             catch (Exception ex)
1237             {
1238                 throw new WebApiException("Err:Download failed", ex);
1239             }
1240         }
1241 #endregion
1242
1243         public bool ReadOwnPost
1244         {
1245             get
1246             {
1247                 return _readOwnPost;
1248             }
1249             set
1250             {
1251                 _readOwnPost = value;
1252             }
1253         }
1254
1255         public int FollowersCount { get; private set; }
1256         public int FriendsCount { get; private set; }
1257         public int StatusesCount { get; private set; }
1258         public string Location { get; private set; } = "";
1259         public string Bio { get; private set; } = "";
1260
1261         /// <summary>ユーザーのフォロワー数などの情報を更新します</summary>
1262         private void UpdateUserStats(TwitterUser self)
1263         {
1264             this.FollowersCount = self.FollowersCount;
1265             this.FriendsCount = self.FriendsCount;
1266             this.StatusesCount = self.StatusesCount;
1267             this.Location = self.Location;
1268             this.Bio = self.Description;
1269         }
1270
1271         /// <summary>
1272         /// 渡された取得件数がWORKERTYPEに応じた取得可能範囲に収まっているか検証する
1273         /// </summary>
1274         public static bool VerifyApiResultCount(MyCommon.WORKERTYPE type, int count)
1275         {
1276             return count >= 20 && count <= GetMaxApiResultCount(type);
1277         }
1278
1279         /// <summary>
1280         /// 渡された取得件数が更新時の取得可能範囲に収まっているか検証する
1281         /// </summary>
1282         public static bool VerifyMoreApiResultCount(int count)
1283         {
1284             return count >= 20 && count <= 200;
1285         }
1286
1287         /// <summary>
1288         /// 渡された取得件数が起動時の取得可能範囲に収まっているか検証する
1289         /// </summary>
1290         public static bool VerifyFirstApiResultCount(int count)
1291         {
1292             return count >= 20 && count <= 200;
1293         }
1294
1295         /// <summary>
1296         /// WORKERTYPEに応じた取得可能な最大件数を取得する
1297         /// </summary>
1298         public static int GetMaxApiResultCount(MyCommon.WORKERTYPE type)
1299         {
1300             // 参照: REST APIs - 各endpointのcountパラメータ
1301             // https://dev.twitter.com/rest/public
1302             switch (type)
1303             {
1304                 case MyCommon.WORKERTYPE.Timeline:
1305                 case MyCommon.WORKERTYPE.Reply:
1306                 case MyCommon.WORKERTYPE.UserTimeline:
1307                 case MyCommon.WORKERTYPE.Favorites:
1308                 case MyCommon.WORKERTYPE.DirectMessegeRcv:
1309                 case MyCommon.WORKERTYPE.DirectMessegeSnt:
1310                 case MyCommon.WORKERTYPE.List:  // 不明
1311                     return 200;
1312
1313                 case MyCommon.WORKERTYPE.PublicSearch:
1314                     return 100;
1315
1316                 default:
1317                     throw new InvalidOperationException("Invalid type: " + type);
1318             }
1319         }
1320
1321         /// <summary>
1322         /// WORKERTYPEに応じた取得件数を取得する
1323         /// </summary>
1324         public static int GetApiResultCount(MyCommon.WORKERTYPE type, bool more, bool startup)
1325         {
1326             if (type == MyCommon.WORKERTYPE.DirectMessegeRcv ||
1327                 type == MyCommon.WORKERTYPE.DirectMessegeSnt)
1328             {
1329                 return 20;
1330             }
1331
1332             if (SettingCommon.Instance.UseAdditionalCount)
1333             {
1334                 switch (type)
1335                 {
1336                     case MyCommon.WORKERTYPE.Favorites:
1337                         if (SettingCommon.Instance.FavoritesCountApi != 0)
1338                             return SettingCommon.Instance.FavoritesCountApi;
1339                         break;
1340                     case MyCommon.WORKERTYPE.List:
1341                         if (SettingCommon.Instance.ListCountApi != 0)
1342                             return SettingCommon.Instance.ListCountApi;
1343                         break;
1344                     case MyCommon.WORKERTYPE.PublicSearch:
1345                         if (SettingCommon.Instance.SearchCountApi != 0)
1346                             return SettingCommon.Instance.SearchCountApi;
1347                         break;
1348                     case MyCommon.WORKERTYPE.UserTimeline:
1349                         if (SettingCommon.Instance.UserTimelineCountApi != 0)
1350                             return SettingCommon.Instance.UserTimelineCountApi;
1351                         break;
1352                 }
1353                 if (more && SettingCommon.Instance.MoreCountApi != 0)
1354                 {
1355                     return Math.Min(SettingCommon.Instance.MoreCountApi, GetMaxApiResultCount(type));
1356                 }
1357                 if (startup && SettingCommon.Instance.FirstCountApi != 0 && type != MyCommon.WORKERTYPE.Reply)
1358                 {
1359                     return Math.Min(SettingCommon.Instance.FirstCountApi, GetMaxApiResultCount(type));
1360                 }
1361             }
1362
1363             // 上記に当てはまらない場合の共通処理
1364             var count = SettingCommon.Instance.CountApi;
1365
1366             if (type == MyCommon.WORKERTYPE.Reply)
1367                 count = SettingCommon.Instance.CountApiReply;
1368
1369             return Math.Min(count, GetMaxApiResultCount(type));
1370         }
1371
1372         public void GetTimelineApi(bool read,
1373                                 MyCommon.WORKERTYPE gType,
1374                                 bool more,
1375                                 bool startup)
1376         {
1377             this.CheckAccountState();
1378
1379             HttpStatusCode res;
1380             var content = "";
1381             var count = GetApiResultCount(gType, more, startup);
1382
1383             try
1384             {
1385                 if (gType == MyCommon.WORKERTYPE.Timeline)
1386                 {
1387                     if (more)
1388                     {
1389                         res = twCon.HomeTimeline(count, this.minHomeTimeline, null, ref content);
1390                     }
1391                     else
1392                     {
1393                         res = twCon.HomeTimeline(count, null, null, ref content);
1394                     }
1395                 }
1396                 else
1397                 {
1398                     if (more)
1399                     {
1400                         res = twCon.Mentions(count, this.minMentions, null, ref content);
1401                     }
1402                     else
1403                     {
1404                         res = twCon.Mentions(count, null, null, ref content);
1405                     }
1406                 }
1407             }
1408             catch(Exception ex)
1409             {
1410                 throw new WebApiException("Err:" + ex.Message, ex);
1411             }
1412
1413             this.CheckStatusCode(res, content);
1414
1415             var minimumId = CreatePostsFromJson(content, gType, null, read);
1416
1417             if (minimumId != null)
1418             {
1419                 if (gType == MyCommon.WORKERTYPE.Timeline)
1420                     this.minHomeTimeline = minimumId.Value;
1421                 else
1422                     this.minMentions = minimumId.Value;
1423             }
1424         }
1425
1426         public void GetUserTimelineApi(bool read,
1427                                          string userName,
1428                                          TabClass tab,
1429                                          bool more)
1430         {
1431             this.CheckAccountState();
1432
1433             HttpStatusCode res;
1434             var content = "";
1435             var count = GetApiResultCount(MyCommon.WORKERTYPE.UserTimeline, more, false);
1436
1437             try
1438             {
1439                 if (string.IsNullOrEmpty(userName))
1440                 {
1441                     var target = tab.User;
1442                     if (string.IsNullOrEmpty(target)) return;
1443                     userName = target;
1444                     res = twCon.UserTimeline(null, target, count, null, null, ref content);
1445                 }
1446                 else
1447                 {
1448                     if (more)
1449                     {
1450                         res = twCon.UserTimeline(null, userName, count, tab.OldestId, null, ref content);
1451                     }
1452                     else
1453                     {
1454                         res = twCon.UserTimeline(null, userName, count, null, null, ref content);
1455                     }
1456                 }
1457             }
1458             catch(Exception ex)
1459             {
1460                 throw new WebApiException("Err:" + ex.Message, ex);
1461             }
1462
1463             if (res == HttpStatusCode.Unauthorized)
1464                 throw new WebApiException("Err:@" + userName + "'s Tweets are protected.");
1465
1466             this.CheckStatusCode(res, content);
1467
1468             var minimumId = CreatePostsFromJson(content, MyCommon.WORKERTYPE.UserTimeline, tab, read);
1469
1470             if (minimumId != null)
1471                 tab.OldestId = minimumId.Value;
1472         }
1473
1474         public PostClass GetStatusApi(bool read, long id)
1475         {
1476             this.CheckAccountState();
1477
1478             HttpStatusCode res;
1479             var content = "";
1480             try
1481             {
1482                 res = twCon.ShowStatuses(id, ref content);
1483             }
1484             catch(Exception ex)
1485             {
1486                 throw new WebApiException("Err:" + ex.Message, ex);
1487             }
1488
1489             if (res == HttpStatusCode.Forbidden)
1490                 throw new WebApiException("Err:protected user's tweet", content);
1491
1492             this.CheckStatusCode(res, content);
1493
1494             TwitterStatus status;
1495             try
1496             {
1497                 status = TwitterStatus.ParseJson(content);
1498             }
1499             catch(SerializationException ex)
1500             {
1501                 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
1502                 throw new WebApiException("Json Parse Error(DataContractJsonSerializer)", content, ex);
1503             }
1504             catch(Exception ex)
1505             {
1506                 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
1507                 throw new WebApiException("Invalid Json!", content, ex);
1508             }
1509
1510             var item = CreatePostsFromStatusData(status);
1511             if (item == null)
1512                 throw new WebApiException("Err:Can't create post", content);
1513
1514             item.IsRead = read;
1515             if (item.IsMe && !read && _readOwnPost) item.IsRead = true;
1516
1517             return item;
1518         }
1519
1520         public void GetStatusApi(bool read, long id, TabClass tab)
1521         {
1522             var post = this.GetStatusApi(read, id);
1523
1524             //非同期アイコン取得&StatusDictionaryに追加
1525             if (tab != null && tab.IsInnerStorageTabType)
1526                 tab.AddPostToInnerStorage(post);
1527             else
1528                 TabInformations.GetInstance().AddPost(post);
1529         }
1530
1531         private PostClass CreatePostsFromStatusData(TwitterStatus status)
1532         {
1533             return CreatePostsFromStatusData(status, false);
1534         }
1535
1536         private PostClass CreatePostsFromStatusData(TwitterStatus status, bool favTweet)
1537         {
1538             var post = new PostClass();
1539             TwitterEntities entities;
1540             string sourceHtml;
1541
1542             post.StatusId = status.Id;
1543             if (status.RetweetedStatus != null)
1544             {
1545                 var retweeted = status.RetweetedStatus;
1546
1547                 post.CreatedAt = MyCommon.DateTimeParse(retweeted.CreatedAt);
1548
1549                 //Id
1550                 post.RetweetedId = retweeted.Id;
1551                 //本文
1552                 post.TextFromApi = retweeted.Text;
1553                 entities = retweeted.MergedEntities;
1554                 sourceHtml = retweeted.Source;
1555                 //Reply先
1556                 post.InReplyToStatusId = retweeted.InReplyToStatusId;
1557                 post.InReplyToUser = retweeted.InReplyToScreenName;
1558                 post.InReplyToUserId = status.InReplyToUserId;
1559
1560                 if (favTweet)
1561                 {
1562                     post.IsFav = true;
1563                 }
1564                 else
1565                 {
1566                     //幻覚fav対策
1567                     var tc = TabInformations.GetInstance().GetTabByType(MyCommon.TabUsageType.Favorites);
1568                     post.IsFav = tc.Contains(retweeted.Id);
1569                 }
1570
1571                 if (retweeted.Coordinates != null)
1572                     post.PostGeo = new PostClass.StatusGeo(retweeted.Coordinates.Coordinates[0], retweeted.Coordinates.Coordinates[1]);
1573
1574                 //以下、ユーザー情報
1575                 var user = retweeted.User;
1576
1577                 if (user == null || user.ScreenName == null || status.User.ScreenName == null) return null;
1578
1579                 post.UserId = user.Id;
1580                 post.ScreenName = user.ScreenName;
1581                 post.Nickname = user.Name.Trim();
1582                 post.ImageUrl = user.ProfileImageUrlHttps;
1583                 post.IsProtect = user.Protected;
1584
1585                 //Retweetした人
1586                 post.RetweetedBy = status.User.ScreenName;
1587                 post.RetweetedByUserId = status.User.Id;
1588                 post.IsMe = post.RetweetedBy.ToLower().Equals(_uname);
1589             }
1590             else
1591             {
1592                 post.CreatedAt = MyCommon.DateTimeParse(status.CreatedAt);
1593                 //本文
1594                 post.TextFromApi = status.Text;
1595                 entities = status.MergedEntities;
1596                 sourceHtml = status.Source;
1597                 post.InReplyToStatusId = status.InReplyToStatusId;
1598                 post.InReplyToUser = status.InReplyToScreenName;
1599                 post.InReplyToUserId = status.InReplyToUserId;
1600
1601                 if (favTweet)
1602                 {
1603                     post.IsFav = true;
1604                 }
1605                 else
1606                 {
1607                     //幻覚fav対策
1608                     var tc = TabInformations.GetInstance().GetTabByType(MyCommon.TabUsageType.Favorites);
1609                     post.IsFav = tc.Contains(post.StatusId) && TabInformations.GetInstance()[post.StatusId].IsFav;
1610                 }
1611
1612                 if (status.Coordinates != null)
1613                     post.PostGeo = new PostClass.StatusGeo(status.Coordinates.Coordinates[0], status.Coordinates.Coordinates[1]);
1614
1615                 //以下、ユーザー情報
1616                 var user = status.User;
1617
1618                 if (user == null || user.ScreenName == null) return null;
1619
1620                 post.UserId = user.Id;
1621                 post.ScreenName = user.ScreenName;
1622                 post.Nickname = user.Name.Trim();
1623                 post.ImageUrl = user.ProfileImageUrlHttps;
1624                 post.IsProtect = user.Protected;
1625                 post.IsMe = post.ScreenName.ToLower().Equals(_uname);
1626             }
1627             //HTMLに整形
1628             string textFromApi = post.TextFromApi;
1629             post.Text = CreateHtmlAnchor(textFromApi, post.ReplyToList, entities, post.Media);
1630             post.TextFromApi = textFromApi;
1631             post.TextFromApi = this.ReplaceTextFromApi(post.TextFromApi, entities);
1632             post.TextFromApi = WebUtility.HtmlDecode(post.TextFromApi);
1633             post.TextFromApi = post.TextFromApi.Replace("<3", "\u2661");
1634
1635             post.QuoteStatusIds = GetQuoteTweetStatusIds(entities)
1636                 .Where(x => x != post.StatusId && x != post.RetweetedId)
1637                 .Distinct().ToArray();
1638
1639             //Source整形
1640             var source = ParseSource(sourceHtml);
1641             post.Source = source.Item1;
1642             post.SourceUri = source.Item2;
1643
1644             post.IsReply = post.ReplyToList.Contains(_uname);
1645             post.IsExcludeReply = false;
1646
1647             if (post.IsMe)
1648             {
1649                 post.IsOwl = false;
1650             }
1651             else
1652             {
1653                 if (followerId.Count > 0) post.IsOwl = !followerId.Contains(post.UserId);
1654             }
1655
1656             post.IsDm = false;
1657             return post;
1658         }
1659
1660         /// <summary>
1661         /// ツイートに含まれる引用ツイートのURLからステータスIDを抽出
1662         /// </summary>
1663         public static IEnumerable<long> GetQuoteTweetStatusIds(IEnumerable<TwitterEntity> entities)
1664         {
1665             var urls = entities.OfType<TwitterEntityUrl>().Where(x => x != null)
1666                 .Select(x => x.ExpandedUrl);
1667
1668             return GetQuoteTweetStatusIds(urls);
1669         }
1670
1671         public static IEnumerable<long> GetQuoteTweetStatusIds(IEnumerable<string> urls)
1672         {
1673             foreach (var url in urls)
1674             {
1675                 var match = Twitter.StatusUrlRegex.Match(url);
1676                 if (match.Success)
1677                 {
1678                     yield return long.Parse(match.Groups["StatusId"].Value);
1679                 }
1680             }
1681         }
1682
1683         private long? CreatePostsFromJson(string content, MyCommon.WORKERTYPE gType, TabClass tab, bool read)
1684         {
1685             TwitterStatus[] items;
1686             try
1687             {
1688                 items = TwitterStatus.ParseJsonArray(content);
1689             }
1690             catch(SerializationException ex)
1691             {
1692                 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
1693                 throw new WebApiException("Json Parse Error(DataContractJsonSerializer)", content, ex);
1694             }
1695             catch(Exception ex)
1696             {
1697                 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
1698                 throw new WebApiException("Invalid Json!", content, ex);
1699             }
1700
1701             long? minimumId = null;
1702
1703             foreach (var status in items)
1704             {
1705                 PostClass post = null;
1706                 post = CreatePostsFromStatusData(status);
1707                 if (post == null) continue;
1708
1709                 if (minimumId == null || minimumId.Value > post.StatusId)
1710                     minimumId = post.StatusId;
1711
1712                 //二重取得回避
1713                 lock (LockObj)
1714                 {
1715                     if (tab == null)
1716                     {
1717                         if (TabInformations.GetInstance().ContainsKey(post.StatusId)) continue;
1718                     }
1719                     else
1720                     {
1721                         if (tab.Contains(post.StatusId)) continue;
1722                     }
1723                 }
1724
1725                 //RT禁止ユーザーによるもの
1726                 if (gType != MyCommon.WORKERTYPE.UserTimeline &&
1727                     post.RetweetedByUserId != null && this.noRTId.Contains(post.RetweetedByUserId.Value)) continue;
1728
1729                 post.IsRead = read;
1730                 if (post.IsMe && !read && _readOwnPost) post.IsRead = true;
1731
1732                 //非同期アイコン取得&StatusDictionaryに追加
1733                 if (tab != null && tab.IsInnerStorageTabType)
1734                     tab.AddPostToInnerStorage(post);
1735                 else
1736                     TabInformations.GetInstance().AddPost(post);
1737             }
1738
1739             return minimumId;
1740         }
1741
1742         private long? CreatePostsFromSearchJson(string content, TabClass tab, bool read, int count, bool more)
1743         {
1744             TwitterSearchResult items;
1745             try
1746             {
1747                 items = TwitterSearchResult.ParseJson(content);
1748             }
1749             catch (SerializationException ex)
1750             {
1751                 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
1752                 throw new WebApiException("Json Parse Error(DataContractJsonSerializer)", content, ex);
1753             }
1754             catch (Exception ex)
1755             {
1756                 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
1757                 throw new WebApiException("Invalid Json!", content, ex);
1758             }
1759
1760             long? minimumId = null;
1761
1762             foreach (var result in items.Statuses)
1763             {
1764                 PostClass post = null;
1765                 post = CreatePostsFromStatusData(result);
1766
1767                 if (post == null)
1768                 {
1769                     // Search API は相変わらずぶっ壊れたデータを返すことがあるため、必要なデータが欠如しているものは取得し直す
1770                     try
1771                     {
1772                         post = this.GetStatusApi(read, result.Id);
1773                     }
1774                     catch (WebApiException)
1775                     {
1776                         continue;
1777                     }
1778                 }
1779
1780                 if (minimumId == null || minimumId.Value > post.StatusId)
1781                     minimumId = post.StatusId;
1782
1783                 if (!more && post.StatusId > tab.SinceId) tab.SinceId = post.StatusId;
1784                 //二重取得回避
1785                 lock (LockObj)
1786                 {
1787                     if (tab == null)
1788                     {
1789                         if (TabInformations.GetInstance().ContainsKey(post.StatusId)) continue;
1790                     }
1791                     else
1792                     {
1793                         if (tab.Contains(post.StatusId)) continue;
1794                     }
1795                 }
1796
1797                 post.IsRead = read;
1798                 if ((post.IsMe && !read) && this._readOwnPost) post.IsRead = true;
1799
1800                 //非同期アイコン取得&StatusDictionaryに追加
1801                 if (tab != null && tab.IsInnerStorageTabType)
1802                     tab.AddPostToInnerStorage(post);
1803                 else
1804                     TabInformations.GetInstance().AddPost(post);
1805             }
1806
1807             return minimumId;
1808         }
1809
1810         private void CreateFavoritePostsFromJson(string content, bool read)
1811         {
1812             TwitterStatus[] item;
1813             try
1814             {
1815                 item = TwitterStatus.ParseJsonArray(content);
1816             }
1817             catch (SerializationException ex)
1818             {
1819                 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
1820                 throw new WebApiException("Json Parse Error(DataContractJsonSerializer)", content, ex);
1821             }
1822             catch (Exception ex)
1823             {
1824                 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
1825                 throw new WebApiException("Invalid Json!", content, ex);
1826             }
1827
1828             var favTab = TabInformations.GetInstance().GetTabByType(MyCommon.TabUsageType.Favorites);
1829
1830             foreach (var status in item)
1831             {
1832                 //二重取得回避
1833                 lock (LockObj)
1834                 {
1835                     if (favTab.Contains(status.Id)) continue;
1836                 }
1837
1838                 var post = CreatePostsFromStatusData(status, true);
1839                 if (post == null) continue;
1840
1841                 post.IsRead = read;
1842
1843                 TabInformations.GetInstance().AddPost(post);
1844             }
1845         }
1846
1847         public void GetListStatus(bool read,
1848                                 TabClass tab,
1849                                 bool more,
1850                                 bool startup)
1851         {
1852             HttpStatusCode res;
1853             var content = "";
1854             var count = GetApiResultCount(MyCommon.WORKERTYPE.List, more, startup);
1855
1856             try
1857             {
1858                 if (more)
1859                 {
1860                     res = twCon.GetListsStatuses(tab.ListInfo.UserId, tab.ListInfo.Id, count, tab.OldestId, null, SettingCommon.Instance.IsListsIncludeRts, ref content);
1861                 }
1862                 else
1863                 {
1864                     res = twCon.GetListsStatuses(tab.ListInfo.UserId, tab.ListInfo.Id, count, null, null, SettingCommon.Instance.IsListsIncludeRts, ref content);
1865                 }
1866             }
1867             catch(Exception ex)
1868             {
1869                 throw new WebApiException("Err:" + ex.Message, ex);
1870             }
1871
1872             this.CheckStatusCode(res, content);
1873
1874             var minimumId = CreatePostsFromJson(content, MyCommon.WORKERTYPE.List, tab, read);
1875
1876             if (minimumId != null)
1877                 tab.OldestId = minimumId.Value;
1878         }
1879
1880         /// <summary>
1881         /// startStatusId からリプライ先の発言を辿る。発言は posts 以外からは検索しない。
1882         /// </summary>
1883         /// <returns>posts の中から検索されたリプライチェインの末端</returns>
1884         internal static PostClass FindTopOfReplyChain(IDictionary<Int64, PostClass> posts, Int64 startStatusId)
1885         {
1886             if (!posts.ContainsKey(startStatusId))
1887                 throw new ArgumentException("startStatusId (" + startStatusId + ") が posts の中から見つかりませんでした。", nameof(startStatusId));
1888
1889             var nextPost = posts[startStatusId];
1890             while (nextPost.InReplyToStatusId != null)
1891             {
1892                 if (!posts.ContainsKey(nextPost.InReplyToStatusId.Value))
1893                     break;
1894                 nextPost = posts[nextPost.InReplyToStatusId.Value];
1895             }
1896
1897             return nextPost;
1898         }
1899
1900         public void GetRelatedResult(bool read, TabClass tab)
1901         {
1902             var relPosts = new Dictionary<Int64, PostClass>();
1903             if (tab.RelationTargetPost.TextFromApi.Contains("@") && tab.RelationTargetPost.InReplyToStatusId == null)
1904             {
1905                 //検索結果対応
1906                 var p = TabInformations.GetInstance()[tab.RelationTargetPost.StatusId];
1907                 if (p != null && p.InReplyToStatusId != null)
1908                 {
1909                     tab.RelationTargetPost = p;
1910                 }
1911                 else
1912                 {
1913                     p = this.GetStatusApi(read, tab.RelationTargetPost.StatusId);
1914                     tab.RelationTargetPost = p;
1915                 }
1916             }
1917             relPosts.Add(tab.RelationTargetPost.StatusId, tab.RelationTargetPost);
1918
1919             Exception lastException = null;
1920
1921             // in_reply_to_status_id を使用してリプライチェインを辿る
1922             var nextPost = FindTopOfReplyChain(relPosts, tab.RelationTargetPost.StatusId);
1923             var loopCount = 1;
1924             while (nextPost.InReplyToStatusId != null && loopCount++ <= 20)
1925             {
1926                 var inReplyToId = nextPost.InReplyToStatusId.Value;
1927
1928                 var inReplyToPost = TabInformations.GetInstance()[inReplyToId];
1929                 if (inReplyToPost == null)
1930                 {
1931                     try
1932                     {
1933                         inReplyToPost = this.GetStatusApi(read, inReplyToId);
1934                     }
1935                     catch (WebApiException ex)
1936                     {
1937                         lastException = ex;
1938                         break;
1939                     }
1940                 }
1941
1942                 relPosts.Add(inReplyToPost.StatusId, inReplyToPost);
1943
1944                 nextPost = FindTopOfReplyChain(relPosts, nextPost.StatusId);
1945             }
1946
1947             //MRTとかに対応のためツイート内にあるツイートを指すURLを取り込む
1948             var text = tab.RelationTargetPost.Text;
1949             var ma = Twitter.StatusUrlRegex.Matches(text).Cast<Match>()
1950                 .Concat(Twitter.ThirdPartyStatusUrlRegex.Matches(text).Cast<Match>());
1951             foreach (var _match in ma)
1952             {
1953                 Int64 _statusId;
1954                 if (Int64.TryParse(_match.Groups["StatusId"].Value, out _statusId))
1955                 {
1956                     if (relPosts.ContainsKey(_statusId))
1957                         continue;
1958
1959                     var p = TabInformations.GetInstance()[_statusId];
1960                     if (p == null)
1961                     {
1962                         try
1963                         {
1964                             p = this.GetStatusApi(read, _statusId);
1965                         }
1966                         catch (WebApiException ex)
1967                         {
1968                             lastException = ex;
1969                             break;
1970                         }
1971                     }
1972
1973                     if (p != null)
1974                         relPosts.Add(p.StatusId, p);
1975                 }
1976             }
1977
1978             relPosts.Values.ToList().ForEach(p =>
1979             {
1980                 if (p.IsMe && !read && this._readOwnPost)
1981                     p.IsRead = true;
1982                 else
1983                     p.IsRead = read;
1984
1985                 tab.AddPostToInnerStorage(p);
1986             });
1987
1988             if (lastException != null)
1989                 throw new WebApiException(lastException.Message, lastException);
1990         }
1991
1992         public void GetSearch(bool read,
1993                             TabClass tab,
1994                             bool more)
1995         {
1996             HttpStatusCode res;
1997             var content = "";
1998             var count = GetApiResultCount(MyCommon.WORKERTYPE.PublicSearch, more, false);
1999             long? maxId = null;
2000             long? sinceId = null;
2001             if (more)
2002             {
2003                 maxId = tab.OldestId - 1;
2004             }
2005             else
2006             {
2007                 sinceId = tab.SinceId;
2008             }
2009
2010             try
2011             {
2012                 // TODO:一時的に40>100件に 件数変更UI作成の必要あり
2013                 res = twCon.Search(tab.SearchWords, tab.SearchLang, count, maxId, sinceId, ref content);
2014             }
2015             catch(Exception ex)
2016             {
2017                 throw new WebApiException("Err:" + ex.Message, ex);
2018             }
2019             switch (res)
2020             {
2021                 case HttpStatusCode.BadRequest:
2022                     throw new WebApiException("Invalid query", content);
2023                 case HttpStatusCode.NotFound:
2024                     throw new WebApiException("Invalid query", content);
2025                 case HttpStatusCode.PaymentRequired: //API Documentには420と書いてあるが、該当コードがないので402にしてある
2026                     throw new WebApiException("Search API Limit?", content);
2027                 case HttpStatusCode.OK:
2028                     break;
2029                 default:
2030                     throw new WebApiException("Err:" + res.ToString() + "(" + MethodBase.GetCurrentMethod().Name + ")", content);
2031             }
2032
2033             if (!TabInformations.GetInstance().ContainsTab(tab))
2034                 return;
2035
2036             var minimumId =  this.CreatePostsFromSearchJson(content, tab, read, count, more);
2037
2038             if (minimumId != null)
2039                 tab.OldestId = minimumId.Value;
2040         }
2041
2042         private void CreateDirectMessagesFromJson(string content, MyCommon.WORKERTYPE gType, bool read)
2043         {
2044             TwitterDirectMessage[] item;
2045             try
2046             {
2047                 if (gType == MyCommon.WORKERTYPE.UserStream)
2048                 {
2049                     item = new[] { TwitterStreamEventDirectMessage.ParseJson(content).DirectMessage };
2050                 }
2051                 else
2052                 {
2053                     item = TwitterDirectMessage.ParseJsonArray(content);
2054                 }
2055             }
2056             catch(SerializationException ex)
2057             {
2058                 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
2059                 throw new WebApiException("Json Parse Error(DataContractJsonSerializer)", content, ex);
2060             }
2061             catch(Exception ex)
2062             {
2063                 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
2064                 throw new WebApiException("Invalid Json!", content, ex);
2065             }
2066
2067             foreach (var message in item)
2068             {
2069                 var post = new PostClass();
2070                 try
2071                 {
2072                     post.StatusId = message.Id;
2073                     if (gType != MyCommon.WORKERTYPE.UserStream)
2074                     {
2075                         if (gType == MyCommon.WORKERTYPE.DirectMessegeRcv)
2076                         {
2077                             if (minDirectmessage > post.StatusId) minDirectmessage = post.StatusId;
2078                         }
2079                         else
2080                         {
2081                             if (minDirectmessageSent > post.StatusId) minDirectmessageSent = post.StatusId;
2082                         }
2083                     }
2084
2085                     //二重取得回避
2086                     lock (LockObj)
2087                     {
2088                         if (TabInformations.GetInstance().GetTabByType(MyCommon.TabUsageType.DirectMessage).Contains(post.StatusId)) continue;
2089                     }
2090                     //sender_id
2091                     //recipient_id
2092                     post.CreatedAt = MyCommon.DateTimeParse(message.CreatedAt);
2093                     //本文
2094                     var textFromApi = message.Text;
2095                     //HTMLに整形
2096                     post.Text = CreateHtmlAnchor(textFromApi, post.ReplyToList, message.Entities, post.Media);
2097                     post.TextFromApi = this.ReplaceTextFromApi(textFromApi, message.Entities);
2098                     post.TextFromApi = WebUtility.HtmlDecode(post.TextFromApi);
2099                     post.TextFromApi = post.TextFromApi.Replace("<3", "\u2661");
2100                     post.IsFav = false;
2101
2102                     post.QuoteStatusIds = GetQuoteTweetStatusIds(message.Entities).Distinct().ToArray();
2103
2104                     //以下、ユーザー情報
2105                     TwitterUser user;
2106                     if (gType == MyCommon.WORKERTYPE.UserStream)
2107                     {
2108                         if (twCon.AuthenticatedUsername.Equals(message.Recipient.ScreenName, StringComparison.CurrentCultureIgnoreCase))
2109                         {
2110                             user = message.Sender;
2111                             post.IsMe = false;
2112                             post.IsOwl = true;
2113                         }
2114                         else
2115                         {
2116                             user = message.Recipient;
2117                             post.IsMe = true;
2118                             post.IsOwl = false;
2119                         }
2120                     }
2121                     else
2122                     {
2123                         if (gType == MyCommon.WORKERTYPE.DirectMessegeRcv)
2124                         {
2125                             user = message.Sender;
2126                             post.IsMe = false;
2127                             post.IsOwl = true;
2128                         }
2129                         else
2130                         {
2131                             user = message.Recipient;
2132                             post.IsMe = true;
2133                             post.IsOwl = false;
2134                         }
2135                     }
2136
2137                     post.UserId = user.Id;
2138                     post.ScreenName = user.ScreenName;
2139                     post.Nickname = user.Name.Trim();
2140                     post.ImageUrl = user.ProfileImageUrlHttps;
2141                     post.IsProtect = user.Protected;
2142                 }
2143                 catch(Exception ex)
2144                 {
2145                     MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
2146                     MessageBox.Show("Parse Error(CreateDirectMessagesFromJson)");
2147                     continue;
2148                 }
2149
2150                 post.IsRead = read;
2151                 if (post.IsMe && !read && _readOwnPost) post.IsRead = true;
2152                 post.IsReply = false;
2153                 post.IsExcludeReply = false;
2154                 post.IsDm = true;
2155
2156                 var dmTab = TabInformations.GetInstance().GetTabByType(MyCommon.TabUsageType.DirectMessage);
2157                 dmTab.AddPostToInnerStorage(post);
2158             }
2159         }
2160
2161         public void GetDirectMessageApi(bool read,
2162                                 MyCommon.WORKERTYPE gType,
2163                                 bool more)
2164         {
2165             this.CheckAccountState();
2166             this.CheckAccessLevel(TwitterApiAccessLevel.ReadWriteAndDirectMessage);
2167
2168             HttpStatusCode res;
2169             var content = "";
2170             var count = GetApiResultCount(gType, more, false);
2171
2172             try
2173             {
2174                 if (gType == MyCommon.WORKERTYPE.DirectMessegeRcv)
2175                 {
2176                     if (more)
2177                     {
2178                         res = twCon.DirectMessages(count, minDirectmessage, null, ref content);
2179                     }
2180                     else
2181                     {
2182                         res = twCon.DirectMessages(count, null, null, ref content);
2183                     }
2184                 }
2185                 else
2186                 {
2187                     if (more)
2188                     {
2189                         res = twCon.DirectMessagesSent(count, minDirectmessageSent, null, ref content);
2190                     }
2191                     else
2192                     {
2193                         res = twCon.DirectMessagesSent(count, null, null, ref content);
2194                     }
2195                 }
2196             }
2197             catch(Exception ex)
2198             {
2199                 throw new WebApiException("Err:" + ex.Message, ex);
2200             }
2201
2202             this.CheckStatusCode(res, content);
2203
2204             CreateDirectMessagesFromJson(content, gType, read);
2205         }
2206
2207         public void GetFavoritesApi(bool read,
2208                             bool more)
2209         {
2210             this.CheckAccountState();
2211
2212             HttpStatusCode res;
2213             var content = "";
2214             var count = GetApiResultCount(MyCommon.WORKERTYPE.Favorites, more, false);
2215
2216             try
2217             {
2218                 res = twCon.Favorites(count, ref content);
2219             }
2220             catch(Exception ex)
2221             {
2222                 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
2223             }
2224
2225             this.CheckStatusCode(res, content);
2226
2227             CreateFavoritePostsFromJson(content, read);
2228         }
2229
2230         private string ReplaceTextFromApi(string text, TwitterEntities entities)
2231         {
2232             if (entities != null)
2233             {
2234                 if (entities.Urls != null)
2235                 {
2236                     foreach (var m in entities.Urls)
2237                     {
2238                         if (!string.IsNullOrEmpty(m.DisplayUrl)) text = text.Replace(m.Url, m.DisplayUrl);
2239                     }
2240                 }
2241                 if (entities.Media != null)
2242                 {
2243                     foreach (var m in entities.Media)
2244                     {
2245                         if (!string.IsNullOrEmpty(m.DisplayUrl)) text = text.Replace(m.Url, m.DisplayUrl);
2246                     }
2247                 }
2248             }
2249             return text;
2250         }
2251
2252         /// <summary>
2253         /// フォロワーIDを更新します
2254         /// </summary>
2255         /// <exception cref="WebApiException"/>
2256         public void RefreshFollowerIds()
2257         {
2258             if (MyCommon._endingFlag) return;
2259
2260             var cursor = -1L;
2261             var newFollowerIds = new HashSet<long>();
2262             do
2263             {
2264                 var ret = this.GetFollowerIdsApi(ref cursor);
2265                 newFollowerIds.UnionWith(ret.Ids);
2266                 cursor = ret.NextCursor;
2267             } while (cursor != 0);
2268
2269             this.followerId = newFollowerIds;
2270             TabInformations.GetInstance().RefreshOwl(this.followerId);
2271
2272             this._GetFollowerResult = true;
2273         }
2274
2275         public bool GetFollowersSuccess
2276         {
2277             get
2278             {
2279                 return _GetFollowerResult;
2280             }
2281         }
2282
2283         private TwitterIds GetFollowerIdsApi(ref long cursor)
2284         {
2285             this.CheckAccountState();
2286
2287             HttpStatusCode res;
2288             var content = "";
2289             try
2290             {
2291                 res = twCon.FollowerIds(cursor, ref content);
2292             }
2293             catch(Exception e)
2294             {
2295                 throw new WebApiException("Err:" + e.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", e);
2296             }
2297
2298             this.CheckStatusCode(res, content);
2299
2300             try
2301             {
2302                 var ret = TwitterIds.ParseJson(content);
2303
2304                 if (ret.Ids == null)
2305                 {
2306                     var ex = new WebApiException("Err: ret.id == null (GetFollowerIdsApi)", content);
2307                     MyCommon.ExceptionOut(ex);
2308                     throw ex;
2309                 }
2310
2311                 return ret;
2312             }
2313             catch(SerializationException e)
2314             {
2315                 var ex = new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, e);
2316                 MyCommon.TraceOut(ex);
2317                 throw ex;
2318             }
2319             catch(Exception e)
2320             {
2321                 var ex = new WebApiException("Err:Invalid Json!", content, e);
2322                 MyCommon.TraceOut(ex);
2323                 throw ex;
2324             }
2325         }
2326
2327         /// <summary>
2328         /// RT 非表示ユーザーを更新します
2329         /// </summary>
2330         /// <exception cref="WebApiException"/>
2331         public void RefreshNoRetweetIds()
2332         {
2333             if (MyCommon._endingFlag) return;
2334
2335             this.noRTId = this.NoRetweetIdsApi();
2336
2337             this._GetNoRetweetResult = true;
2338         }
2339
2340         private long[] NoRetweetIdsApi()
2341         {
2342             this.CheckAccountState();
2343
2344             HttpStatusCode res;
2345             var content = "";
2346             try
2347             {
2348                 res = twCon.NoRetweetIds(ref content);
2349             }
2350             catch(Exception e)
2351             {
2352                 throw new WebApiException("Err:" + e.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", e);
2353             }
2354
2355             this.CheckStatusCode(res, content);
2356
2357             try
2358             {
2359                 return MyCommon.CreateDataFromJson<long[]>(content);
2360             }
2361             catch(SerializationException e)
2362             {
2363                 var ex = new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, e);
2364                 MyCommon.TraceOut(ex);
2365                 throw ex;
2366             }
2367             catch(Exception e)
2368             {
2369                 var ex = new WebApiException("Err:Invalid Json!", content, e);
2370                 MyCommon.TraceOut(ex);
2371                 throw ex;
2372             }
2373         }
2374
2375         public bool GetNoRetweetSuccess
2376         {
2377             get
2378             {
2379                 return _GetNoRetweetResult;
2380             }
2381         }
2382
2383         /// <summary>
2384         /// t.co の文字列長などの設定情報を更新します
2385         /// </summary>
2386         /// <exception cref="WebApiException"/>
2387         public void RefreshConfiguration()
2388         {
2389             this.Configuration = this.ConfigurationApi();
2390         }
2391
2392         private TwitterConfiguration ConfigurationApi()
2393         {
2394             HttpStatusCode res;
2395             var content = "";
2396             try
2397             {
2398                 res = twCon.GetConfiguration(ref content);
2399             }
2400             catch(Exception e)
2401             {
2402                 throw new WebApiException("Err:" + e.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", e);
2403             }
2404
2405             this.CheckStatusCode(res, content);
2406
2407             try
2408             {
2409                 return TwitterConfiguration.ParseJson(content);
2410             }
2411             catch(SerializationException e)
2412             {
2413                 var ex = new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, e);
2414                 MyCommon.TraceOut(ex);
2415                 throw ex;
2416             }
2417             catch(Exception e)
2418             {
2419                 var ex = new WebApiException("Err:Invalid Json!", content, e);
2420                 MyCommon.TraceOut(ex);
2421                 throw ex;
2422             }
2423         }
2424
2425         public void GetListsApi()
2426         {
2427             this.CheckAccountState();
2428
2429             HttpStatusCode res;
2430             IEnumerable<ListElement> lists;
2431             var content = "";
2432
2433             try
2434             {
2435                 res = twCon.GetLists(this.Username, ref content);
2436             }
2437             catch (Exception ex)
2438             {
2439                 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
2440             }
2441
2442             this.CheckStatusCode(res, content);
2443
2444             try
2445             {
2446                 lists = TwitterList.ParseJsonArray(content)
2447                     .Select(x => new ListElement(x, this));
2448             }
2449             catch (SerializationException ex)
2450             {
2451                 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
2452                 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
2453             }
2454             catch (Exception ex)
2455             {
2456                 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
2457                 throw new WebApiException("Err:Invalid Json!", content, ex);
2458             }
2459
2460             try
2461             {
2462                 res = twCon.GetListsSubscriptions(this.Username, ref content);
2463             }
2464             catch (Exception ex)
2465             {
2466                 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
2467             }
2468
2469             this.CheckStatusCode(res, content);
2470
2471             try
2472             {
2473                 lists = lists.Concat(TwitterList.ParseJsonArray(content)
2474                     .Select(x => new ListElement(x, this)));
2475             }
2476             catch (SerializationException ex)
2477             {
2478                 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
2479                 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
2480             }
2481             catch (Exception ex)
2482             {
2483                 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
2484                 throw new WebApiException("Err:Invalid Json!", content, ex);
2485             }
2486
2487             TabInformations.GetInstance().SubscribableLists = lists.ToList();
2488         }
2489
2490         public void DeleteList(string list_id)
2491         {
2492             HttpStatusCode res;
2493             var content = "";
2494
2495             try
2496             {
2497                 res = twCon.DeleteListID(this.Username, list_id, ref content);
2498             }
2499             catch(Exception ex)
2500             {
2501                 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
2502             }
2503
2504             this.CheckStatusCode(res, content);
2505         }
2506
2507         public ListElement EditList(string list_id, string new_name, bool isPrivate, string description)
2508         {
2509             HttpStatusCode res;
2510             var content = "";
2511
2512             try
2513             {
2514                 res = twCon.UpdateListID(this.Username, list_id, new_name, isPrivate, description, ref content);
2515             }
2516             catch(Exception ex)
2517             {
2518                 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
2519             }
2520
2521             this.CheckStatusCode(res, content);
2522
2523             try
2524             {
2525                 var le = TwitterList.ParseJson(content);
2526                 return  new ListElement(le, this);
2527             }
2528             catch(SerializationException ex)
2529             {
2530                 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
2531                 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
2532             }
2533             catch(Exception ex)
2534             {
2535                 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
2536                 throw new WebApiException("Err:Invalid Json!", content, ex);
2537             }
2538         }
2539
2540         public long GetListMembers(string list_id, List<UserInfo> lists, long cursor)
2541         {
2542             this.CheckAccountState();
2543
2544             HttpStatusCode res;
2545             var content = "";
2546             try
2547             {
2548                 res = twCon.GetListMembers(this.Username, list_id, cursor, ref content);
2549             }
2550             catch(Exception ex)
2551             {
2552                 throw new WebApiException("Err:" + ex.Message);
2553             }
2554
2555             this.CheckStatusCode(res, content);
2556
2557             try
2558             {
2559                 var users = TwitterUsers.ParseJson(content);
2560                 Array.ForEach<TwitterUser>(
2561                     users.Users,
2562                     u => lists.Add(new UserInfo(u)));
2563
2564                 return users.NextCursor;
2565             }
2566             catch(SerializationException ex)
2567             {
2568                 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
2569                 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
2570             }
2571             catch(Exception ex)
2572             {
2573                 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
2574                 throw new WebApiException("Err:Invalid Json!", content, ex);
2575             }
2576         }
2577
2578         public void CreateListApi(string listName, bool isPrivate, string description)
2579         {
2580             this.CheckAccountState();
2581
2582             HttpStatusCode res;
2583             var content = "";
2584             try
2585             {
2586                 res = twCon.CreateLists(listName, isPrivate, description, ref content);
2587             }
2588             catch(Exception ex)
2589             {
2590                 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
2591             }
2592
2593             this.CheckStatusCode(res, content);
2594
2595             try
2596             {
2597                 var le = TwitterList.ParseJson(content);
2598                 TabInformations.GetInstance().SubscribableLists.Add(new ListElement(le, this));
2599             }
2600             catch(SerializationException ex)
2601             {
2602                 MyCommon.TraceOut(ex.Message + Environment.NewLine + content);
2603                 throw new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
2604             }
2605             catch(Exception ex)
2606             {
2607                 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
2608                 throw new WebApiException("Err:Invalid Json!", content, ex);
2609             }
2610         }
2611
2612         public bool ContainsUserAtList(string listId, string user)
2613         {
2614             this.CheckAccountState();
2615
2616             HttpStatusCode res;
2617             var content = "";
2618
2619             try
2620             {
2621                 res = this.twCon.ShowListMember(listId, user, ref content);
2622             }
2623             catch(Exception ex)
2624             {
2625                 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
2626             }
2627
2628             if (res == HttpStatusCode.NotFound)
2629             {
2630                 return false;
2631             }
2632
2633             this.CheckStatusCode(res, content);
2634
2635             try
2636             {
2637                 TwitterUser.ParseJson(content);
2638                 return true;
2639             }
2640             catch(Exception)
2641             {
2642                 return false;
2643             }
2644         }
2645
2646         public void AddUserToList(string listId, string user)
2647         {
2648             HttpStatusCode res;
2649             var content = "";
2650
2651             try
2652             {
2653                 res = twCon.CreateListMembers(listId, user, ref content);
2654             }
2655             catch(Exception ex)
2656             {
2657                 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
2658             }
2659
2660             this.CheckStatusCode(res, content);
2661         }
2662
2663         public void RemoveUserToList(string listId, string user)
2664         {
2665             HttpStatusCode res;
2666             var content = "";
2667
2668             try
2669             {
2670                 res = twCon.DeleteListMembers(listId, user, ref content);
2671             }
2672             catch(Exception ex)
2673             {
2674                 throw new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", ex);
2675             }
2676
2677             this.CheckStatusCode(res, content);
2678         }
2679
2680         private class range
2681         {
2682             public int fromIndex { get; set; }
2683             public int toIndex { get; set; }
2684             public range(int fromIndex, int toIndex)
2685             {
2686                 this.fromIndex = fromIndex;
2687                 this.toIndex = toIndex;
2688             }
2689         }
2690         public async Task<string> CreateHtmlAnchorAsync(string Text, List<string> AtList, Dictionary<string, string> media)
2691         {
2692             if (Text == null) return null;
2693             var retStr = Text.Replace("&gt;", "<<<<<tweenだいなり>>>>>").Replace("&lt;", "<<<<<tweenしょうなり>>>>>");
2694             //uriの正規表現
2695             //const string url_valid_domain = "(?<domain>(?:[^\p{P}\s][\.\-_](?=[^\p{P}\s])|[^\p{P}\s]){1,}\.[a-z]{2,}(?::[0-9]+)?)"
2696             //const string url_valid_general_path_chars = "[a-z0-9!*';:=+$/%#\[\]\-_,~]"
2697             //const string url_balance_parens = "(?:\(" + url_valid_general_path_chars + "+\))"
2698             //const string url_valid_url_path_ending_chars = "(?:[a-z0-9=_#/\-\+]+|" + url_balance_parens + ")"
2699             //const string pth = "(?:" + url_balance_parens +
2700             //    "|@" + url_valid_general_path_chars + "+/" +
2701             //    "|[.,]?" + url_valid_general_path_chars + "+" +
2702             //    ")"
2703             //const string pth2 = "(/(?:" +
2704             //    pth + "+" + url_valid_url_path_ending_chars + "|" +
2705             //    pth + "+" + url_valid_url_path_ending_chars + "?|" +
2706             //    url_valid_url_path_ending_chars +
2707             //    ")?)?"
2708             //const string qry = "(?<query>\?[a-z0-9!*'();:&=+$/%#\[\]\-_.,~]*[a-z0-9_&=#])?"
2709             //const string rgUrl = "(?<before>(?:[^\""':!=#]|^|\:/))" +
2710             //                            "(?<url>(?<protocol>https?://)" +
2711             //                            url_valid_domain +
2712             //                            pth2 +
2713             //                            qry +
2714             //                            ")"
2715             //const string rgUrl = "(?<before>(?:[^\""':!=#]|^|\:/))" +
2716             //                            "(?<url>(?<protocol>https?://|www\.)" +
2717             //                            url_valid_domain +
2718             //                            pth2 +
2719             //                            qry +
2720             //                            ")"
2721             //絶対パス表現のUriをリンクに置換
2722             retStr = await new Regex(rgUrl, RegexOptions.IgnoreCase).ReplaceAsync(retStr, async mu =>
2723             {
2724                 var sb = new StringBuilder(mu.Result("${before}<a href=\""));
2725                 //if (mu.Result("${protocol}").StartsWith("w", StringComparison.OrdinalIgnoreCase))
2726                 //    sb.Append("http://");
2727                 //}
2728                 var url = mu.Result("${url}");
2729                 var title = await ShortUrl.Instance.ExpandUrlAsync(url);
2730                 sb.Append(url + "\" title=\"" + MyCommon.ConvertToReadableUrl(title) + "\">").Append(url).Append("</a>");
2731                 if (media != null && !media.ContainsKey(url)) media.Add(url, title);
2732                 return sb.ToString();
2733             });
2734
2735             //@先をリンクに置換(リスト)
2736             retStr = Regex.Replace(retStr,
2737                                    @"(^|[^a-zA-Z0-9_/])([@@]+)([a-zA-Z0-9_]{1,20}/[a-zA-Z][a-zA-Z0-9\p{IsLatin-1Supplement}\-]{0,79})",
2738                                    "$1$2<a href=\"/$3\">$3</a>");
2739
2740             var m = Regex.Match(retStr, "(^|[^a-zA-Z0-9_])[@@]([a-zA-Z0-9_]{1,20})");
2741             while (m.Success)
2742             {
2743                 if (!AtList.Contains(m.Result("$2").ToLower())) AtList.Add(m.Result("$2").ToLower());
2744                 m = m.NextMatch();
2745             }
2746             //@先をリンクに置換
2747             retStr = Regex.Replace(retStr,
2748                                    "(^|[^a-zA-Z0-9_/])([@@])([a-zA-Z0-9_]{1,20})",
2749                                    "$1$2<a href=\"/$3\">$3</a>");
2750
2751             //ハッシュタグを抽出し、リンクに置換
2752             var anchorRange = new List<range>();
2753             for (int i = 0; i < retStr.Length; i++)
2754             {
2755                 var index = retStr.IndexOf("<a ", i);
2756                 if (index > -1 && index < retStr.Length)
2757                 {
2758                     i = index;
2759                     var toIndex = retStr.IndexOf("</a>", index);
2760                     if (toIndex > -1)
2761                     {
2762                         anchorRange.Add(new range(index, toIndex + 3));
2763                         i = toIndex;
2764                     }
2765                 }
2766             }
2767             //retStr = Regex.Replace(retStr,
2768             //                       "(^|[^a-zA-Z0-9/&])([##])([0-9a-zA-Z_]*[a-zA-Z_]+[a-zA-Z0-9_\xc0-\xd6\xd8-\xf6\xf8-\xff]*)",
2769             //                       new MatchEvaluator(Function(mh As Match)
2770             //                                              foreach (var rng in anchorRange)
2771             //                                              {
2772             //                                                  if (mh.Index >= rng.fromIndex &&
2773             //                                                   mh.Index <= rng.toIndex) return mh.Result("$0");
2774             //                                              }
2775             //                                              if (IsNumeric(mh.Result("$3"))) return mh.Result("$0");
2776             //                                              lock (LockObj)
2777             //                                              {
2778             //                                                  _hashList.Add("#" + mh.Result("$3"))
2779             //                                              }
2780             //                                              return mh.Result("$1") + "<a href=\"" + _protocol + "twitter.com/search?q=%23" + mh.Result("$3") + "\">" + mh.Result("$2$3") + "</a>";
2781             //                                          }),
2782             //                                      RegexOptions.IgnoreCase)
2783             retStr = Regex.Replace(retStr,
2784                                    HASHTAG,
2785                                    new MatchEvaluator(mh =>
2786                                                       {
2787                                                           foreach (var rng in anchorRange)
2788                                                           {
2789                                                               if (mh.Index >= rng.fromIndex &&
2790                                                                mh.Index <= rng.toIndex) return mh.Result("$0");
2791                                                           }
2792                                                           lock (LockObj)
2793                                                           {
2794                                                               _hashList.Add("#" + mh.Result("$3"));
2795                                                           }
2796                                                           return mh.Result("$1") + "<a href=\"https://twitter.com/search?q=%23" + mh.Result("$3") + "\">" + mh.Result("$2$3") + "</a>";
2797                                                       }),
2798                                                   RegexOptions.IgnoreCase);
2799
2800
2801             retStr = Regex.Replace(retStr, "(^|[^a-zA-Z0-9_/&##@@>=.~])(sm|nm)([0-9]{1,10})", "$1<a href=\"http://www.nicovideo.jp/watch/$2$3\">$2$3</a>");
2802
2803             retStr = retStr.Replace("<<<<<tweenだいなり>>>>>", "&gt;").Replace("<<<<<tweenしょうなり>>>>>", "&lt;");
2804
2805             //retStr = AdjustHtml(ShortUrl.Resolve(PreProcessUrl(retStr), true)) //IDN置換、短縮Uri解決、@リンクを相対→絶対にしてtarget属性付与
2806             retStr = AdjustHtml(PreProcessUrl(retStr)); //IDN置換、短縮Uri解決、@リンクを相対→絶対にしてtarget属性付与
2807             return retStr;
2808         }
2809
2810         public async Task<string> CreateHtmlAnchorAsync(string text, List<string> AtList, TwitterEntities entities, List<MediaInfo> media)
2811         {
2812             if (entities != null)
2813             {
2814                 if (entities.Urls != null)
2815                 {
2816                     foreach (var ent in entities.Urls)
2817                     {
2818                         ent.ExpandedUrl = await ShortUrl.Instance.ExpandUrlAsync(ent.ExpandedUrl)
2819                             .ConfigureAwait(false);
2820
2821                         if (media != null && !media.Any(info => info.Url == ent.ExpandedUrl))
2822                             media.Add(new MediaInfo(ent.ExpandedUrl));
2823                     }
2824                 }
2825                 if (entities.Hashtags != null)
2826                 {
2827                     lock (this.LockObj)
2828                     {
2829                         this._hashList.AddRange(entities.Hashtags.Select(x => "#" + x.Text));
2830                     }
2831                 }
2832                 if (entities.UserMentions != null)
2833                 {
2834                     foreach (var ent in entities.UserMentions)
2835                     {
2836                         var screenName = ent.ScreenName.ToLower();
2837                         if (!AtList.Contains(screenName))
2838                             AtList.Add(screenName);
2839                     }
2840                 }
2841                 if (entities.Media != null)
2842                 {
2843                     if (media != null)
2844                     {
2845                         foreach (var ent in entities.Media)
2846                         {
2847                             if (!media.Any(x => x.Url == ent.MediaUrl))
2848                             {
2849                                 if (ent.VideoInfo != null &&
2850                                     ent.Type == "animated_gif" || ent.Type == "video")
2851                                 {
2852                                     //var videoUrl = ent.VideoInfo.Variants
2853                                     //    .Where(v => v.ContentType == "video/mp4")
2854                                     //    .OrderByDescending(v => v.Bitrate)
2855                                     //    .Select(v => v.Url).FirstOrDefault();
2856                                     media.Add(new MediaInfo(ent.MediaUrl, ent.ExpandedUrl));
2857                                 }
2858                                 else
2859                                     media.Add(new MediaInfo(ent.MediaUrl));
2860                             }
2861                         }
2862                     }
2863                 }
2864             }
2865
2866             text = TweetFormatter.AutoLinkHtml(text, entities);
2867
2868             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>");
2869             text = PreProcessUrl(text); //IDN置換
2870
2871             return text;
2872         }
2873
2874         [Obsolete]
2875         public string CreateHtmlAnchor(string text, List<string> AtList, TwitterEntities entities, List<MediaInfo> media)
2876         {
2877             return this.CreateHtmlAnchorAsync(text, AtList, entities, media).Result;
2878         }
2879
2880         /// <summary>
2881         /// Twitter APIから得たHTML形式のsource文字列を分析し、source名とURLに分離します
2882         /// </summary>
2883         public static Tuple<string, Uri> ParseSource(string sourceHtml)
2884         {
2885             if (string.IsNullOrEmpty(sourceHtml))
2886                 return Tuple.Create<string, Uri>("", null);
2887
2888             string sourceText;
2889             Uri sourceUri;
2890
2891             // sourceHtmlの例: <a href="http://twitter.com" rel="nofollow">Twitter Web Client</a>
2892
2893             var match = Regex.Match(sourceHtml, "^<a href=\"(?<uri>.+?)\".*?>(?<text>.+)</a>$", RegexOptions.IgnoreCase);
2894             if (match.Success)
2895             {
2896                 sourceText = WebUtility.HtmlDecode(match.Groups["text"].Value);
2897                 try
2898                 {
2899                     var uriStr = WebUtility.HtmlDecode(match.Groups["uri"].Value);
2900                     sourceUri = new Uri(new Uri("https://twitter.com/"), uriStr);
2901                 }
2902                 catch (UriFormatException)
2903                 {
2904                     sourceUri = null;
2905                 }
2906             }
2907             else
2908             {
2909                 sourceText = WebUtility.HtmlDecode(sourceHtml);
2910                 sourceUri = null;
2911             }
2912
2913             return Tuple.Create(sourceText, sourceUri);
2914         }
2915
2916         public TwitterApiStatus GetInfoApi()
2917         {
2918             if (Twitter.AccountState != MyCommon.ACCOUNT_STATE.Valid) return null;
2919
2920             if (MyCommon._endingFlag) return null;
2921
2922             HttpStatusCode res;
2923             var content = "";
2924             try
2925             {
2926                 res = twCon.RateLimitStatus(ref content);
2927             }
2928             catch (Exception)
2929             {
2930                 this.ResetApiStatus();
2931                 return null;
2932             }
2933
2934             this.CheckStatusCode(res, content);
2935
2936             try
2937             {
2938                 MyCommon.TwitterApiInfo.UpdateFromJson(content);
2939                 return MyCommon.TwitterApiInfo;
2940             }
2941             catch (Exception ex)
2942             {
2943                 MyCommon.TraceOut(ex, MethodBase.GetCurrentMethod().Name + " " + content);
2944                 MyCommon.TwitterApiInfo.Reset();
2945                 return null;
2946             }
2947         }
2948
2949         /// <summary>
2950         /// ブロック中のユーザーを更新します
2951         /// </summary>
2952         /// <exception cref="WebApiException"/>
2953         public void RefreshBlockIds()
2954         {
2955             if (MyCommon._endingFlag) return;
2956
2957             var cursor = -1L;
2958             var newBlockIds = new HashSet<long>();
2959             do
2960             {
2961                 var ret = this.GetBlockIdsApi(cursor);
2962                 newBlockIds.UnionWith(ret.Ids);
2963                 cursor = ret.NextCursor;
2964             } while (cursor != 0);
2965
2966             newBlockIds.Remove(this.UserId); // 元のソースにあったので一応残しておく
2967
2968             TabInformations.GetInstance().BlockIds = newBlockIds;
2969         }
2970
2971         public TwitterIds GetBlockIdsApi(long cursor)
2972         {
2973             this.CheckAccountState();
2974
2975             HttpStatusCode res;
2976             var content = "";
2977             try
2978             {
2979                 res = twCon.GetBlockUserIds(ref content, cursor);
2980             }
2981             catch(Exception e)
2982             {
2983                 throw new WebApiException("Err:" + e.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", e);
2984             }
2985
2986             this.CheckStatusCode(res, content);
2987
2988             try
2989             {
2990                 return TwitterIds.ParseJson(content);
2991             }
2992             catch(SerializationException e)
2993             {
2994                 var ex = new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, e);
2995                 MyCommon.TraceOut(ex);
2996                 throw ex;
2997             }
2998             catch(Exception e)
2999             {
3000                 var ex = new WebApiException("Err:Invalid Json!", content, e);
3001                 MyCommon.TraceOut(ex);
3002                 throw ex;
3003             }
3004         }
3005
3006         /// <summary>
3007         /// ミュート中のユーザーIDを更新します
3008         /// </summary>
3009         /// <exception cref="WebApiException"/>
3010         public async Task RefreshMuteUserIdsAsync()
3011         {
3012             if (MyCommon._endingFlag) return;
3013
3014             var ids = await TwitterIds.GetAllItemsAsync(this.GetMuteUserIdsApiAsync)
3015                 .ConfigureAwait(false);
3016
3017             TabInformations.GetInstance().MuteUserIds = new HashSet<long>(ids);
3018         }
3019
3020         public async Task<TwitterIds> GetMuteUserIdsApiAsync(long cursor)
3021         {
3022             var content = "";
3023
3024             try
3025             {
3026                 var res = await Task.Run(() => twCon.GetMuteUserIds(ref content, cursor))
3027                     .ConfigureAwait(false);
3028
3029                 this.CheckStatusCode(res, content);
3030
3031                 return TwitterIds.ParseJson(content);
3032             }
3033             catch (WebException ex)
3034             {
3035                 var ex2 = new WebApiException("Err:" + ex.Message + "(" + MethodBase.GetCurrentMethod().Name + ")", content, ex);
3036                 MyCommon.TraceOut(ex2);
3037                 throw ex2;
3038             }
3039             catch (SerializationException ex)
3040             {
3041                 var ex2 = new WebApiException("Err:Json Parse Error(DataContractJsonSerializer)", content, ex);
3042                 MyCommon.TraceOut(ex2);
3043                 throw ex2;
3044             }
3045         }
3046
3047         public string[] GetHashList()
3048         {
3049             string[] hashArray;
3050             lock (LockObj)
3051             {
3052                 hashArray = _hashList.ToArray();
3053                 _hashList.Clear();
3054             }
3055             return hashArray;
3056         }
3057
3058         public string AccessToken
3059         {
3060             get
3061             {
3062                 return twCon.AccessToken;
3063             }
3064         }
3065
3066         public string AccessTokenSecret
3067         {
3068             get
3069             {
3070                 return twCon.AccessTokenSecret;
3071             }
3072         }
3073
3074         private void CheckAccountState()
3075         {
3076             if (Twitter.AccountState != MyCommon.ACCOUNT_STATE.Valid)
3077                 throw new WebApiException("Auth error. Check your account");
3078         }
3079
3080         private void CheckAccessLevel(TwitterApiAccessLevel accessLevelFlags)
3081         {
3082             if (!this.AccessLevel.HasFlag(accessLevelFlags))
3083                 throw new WebApiException("Auth Err:try to re-authorization.");
3084         }
3085
3086         private void CheckStatusCode(HttpStatusCode httpStatus, string responseText,
3087             [CallerMemberName] string callerMethodName = "")
3088         {
3089             if (httpStatus == HttpStatusCode.OK)
3090             {
3091                 Twitter.AccountState = MyCommon.ACCOUNT_STATE.Valid;
3092                 return;
3093             }
3094
3095             if (string.IsNullOrWhiteSpace(responseText))
3096             {
3097                 if (httpStatus == HttpStatusCode.Unauthorized)
3098                     Twitter.AccountState = MyCommon.ACCOUNT_STATE.Invalid;
3099
3100                 throw new WebApiException("Err:" + httpStatus + "(" + callerMethodName + ")");
3101             }
3102
3103             try
3104             {
3105                 var errors = TwitterError.ParseJson(responseText).Errors;
3106                 if (errors == null || !errors.Any())
3107                 {
3108                     throw new WebApiException("Err:" + httpStatus + "(" + callerMethodName + ")", responseText);
3109                 }
3110
3111                 foreach (var error in errors)
3112                 {
3113                     if (error.Code == TwitterErrorCode.InvalidToken ||
3114                         error.Code == TwitterErrorCode.SuspendedAccount)
3115                     {
3116                         Twitter.AccountState = MyCommon.ACCOUNT_STATE.Invalid;
3117                     }
3118                 }
3119
3120                 throw new WebApiException("Err:" + string.Join(",", errors.Select(x => x.ToString())) + "(" + callerMethodName + ")", responseText);
3121             }
3122             catch (SerializationException) { }
3123
3124             throw new WebApiException("Err:" + httpStatus + "(" + callerMethodName + ")", responseText);
3125         }
3126
3127         public int GetTextLengthRemain(string postText)
3128         {
3129             var matchDm = Twitter.DMSendTextRegex.Match(postText);
3130             if (matchDm.Success)
3131                 return this.GetTextLengthRemainInternal(matchDm.Groups["body"].Value, isDm: true);
3132
3133             return this.GetTextLengthRemainInternal(postText, isDm: false);
3134         }
3135
3136         private int GetTextLengthRemainInternal(string postText, bool isDm)
3137         {
3138             var textLength = 0;
3139
3140             var pos = 0;
3141             while (pos < postText.Length)
3142             {
3143                 textLength++;
3144
3145                 if (char.IsSurrogatePair(postText, pos))
3146                     pos += 2; // サロゲートペアの場合は2文字分進める
3147                 else
3148                     pos++;
3149             }
3150
3151             var urlMatches = Regex.Matches(postText, Twitter.rgUrl, RegexOptions.IgnoreCase).Cast<Match>();
3152             foreach (var m in urlMatches)
3153             {
3154                 var before = m.Groups["before"].Value;
3155                 var url = m.Groups["url"].Value;
3156                 var protocol = m.Groups["protocol"].Value;
3157                 var domain = m.Groups["domain"].Value;
3158                 var path = m.Groups["path"].Value;
3159                 if (protocol.Length == 0)
3160                 {
3161                     if (Regex.IsMatch(before, Twitter.url_invalid_without_protocol_preceding_chars))
3162                         continue;
3163
3164                     var validUrl = false;
3165                     string lasturl = null;
3166
3167                     var last_url_invalid_match = false;
3168                     var domainMatches = Regex.Matches(domain, Twitter.url_valid_ascii_domain, RegexOptions.IgnoreCase).Cast<Match>();
3169                     foreach (var mm in domainMatches)
3170                     {
3171                         lasturl = mm.Value;
3172                         last_url_invalid_match = Regex.IsMatch(lasturl, Twitter.url_invalid_short_domain, RegexOptions.IgnoreCase);
3173                         if (!last_url_invalid_match)
3174                         {
3175                             validUrl = true;
3176                         }
3177                     }
3178
3179                     if (last_url_invalid_match && path.Length != 0)
3180                     {
3181                         validUrl = true;
3182                     }
3183
3184                     if (validUrl)
3185                     {
3186                         textLength += this.Configuration.ShortUrlLength - url.Length;
3187                     }
3188                 }
3189                 else
3190                 {
3191                     var shortUrlLength = protocol == "https://"
3192                         ? this.Configuration.ShortUrlLengthHttps
3193                         : this.Configuration.ShortUrlLength;
3194
3195                     textLength += shortUrlLength - url.Length;
3196                 }
3197             }
3198
3199             if (isDm)
3200                 return this.Configuration.DmTextCharacterLimit - textLength;
3201             else
3202                 return 140 - textLength;
3203         }
3204
3205 #region "UserStream"
3206         private string trackWord_ = "";
3207         public string TrackWord
3208         {
3209             get
3210             {
3211                 return trackWord_;
3212             }
3213             set
3214             {
3215                 trackWord_ = value;
3216             }
3217         }
3218         private bool allAtReply_ = false;
3219         public bool AllAtReply
3220         {
3221             get
3222             {
3223                 return allAtReply_;
3224             }
3225             set
3226             {
3227                 allAtReply_ = value;
3228             }
3229         }
3230
3231         public event EventHandler NewPostFromStream;
3232         public event EventHandler UserStreamStarted;
3233         public event EventHandler UserStreamStopped;
3234         public event EventHandler<PostDeletedEventArgs> PostDeleted;
3235         public event EventHandler<UserStreamEventReceivedEventArgs> UserStreamEventReceived;
3236         private DateTime _lastUserstreamDataReceived;
3237         private TwitterUserstream userStream;
3238
3239         public class FormattedEvent
3240         {
3241             public MyCommon.EVENTTYPE Eventtype { get; set; }
3242             public DateTime CreatedAt { get; set; }
3243             public string Event { get; set; }
3244             public string Username { get; set; }
3245             public string Target { get; set; }
3246             public Int64 Id { get; set; }
3247             public bool IsMe { get; set; }
3248         }
3249
3250         public List<FormattedEvent> storedEvent_ = new List<FormattedEvent>();
3251         public List<FormattedEvent> StoredEvent
3252         {
3253             get
3254             {
3255                 return storedEvent_;
3256             }
3257             set
3258             {
3259                 storedEvent_ = value;
3260             }
3261         }
3262
3263         private readonly IReadOnlyDictionary<string, MyCommon.EVENTTYPE> eventTable = new Dictionary<string, MyCommon.EVENTTYPE>
3264         {
3265             ["favorite"] = MyCommon.EVENTTYPE.Favorite,
3266             ["unfavorite"] = MyCommon.EVENTTYPE.Unfavorite,
3267             ["follow"] = MyCommon.EVENTTYPE.Follow,
3268             ["list_member_added"] = MyCommon.EVENTTYPE.ListMemberAdded,
3269             ["list_member_removed"] = MyCommon.EVENTTYPE.ListMemberRemoved,
3270             ["block"] = MyCommon.EVENTTYPE.Block,
3271             ["unblock"] = MyCommon.EVENTTYPE.Unblock,
3272             ["user_update"] = MyCommon.EVENTTYPE.UserUpdate,
3273             ["deleted"] = MyCommon.EVENTTYPE.Deleted,
3274             ["list_created"] = MyCommon.EVENTTYPE.ListCreated,
3275             ["list_destroyed"] = MyCommon.EVENTTYPE.ListDestroyed,
3276             ["list_updated"] = MyCommon.EVENTTYPE.ListUpdated,
3277             ["unfollow"] = MyCommon.EVENTTYPE.Unfollow,
3278             ["list_user_subscribed"] = MyCommon.EVENTTYPE.ListUserSubscribed,
3279             ["list_user_unsubscribed"] = MyCommon.EVENTTYPE.ListUserUnsubscribed,
3280             ["mute"] = MyCommon.EVENTTYPE.Mute,
3281             ["unmute"] = MyCommon.EVENTTYPE.Unmute,
3282             ["quoted_tweet"] = MyCommon.EVENTTYPE.QuotedTweet,
3283         };
3284
3285         public bool IsUserstreamDataReceived
3286         {
3287             get
3288             {
3289                 return DateTime.Now.Subtract(this._lastUserstreamDataReceived).TotalSeconds < 31;
3290             }
3291         }
3292
3293         private void userStream_StatusArrived(string line)
3294         {
3295             this._lastUserstreamDataReceived = DateTime.Now;
3296             if (string.IsNullOrEmpty(line)) return;
3297
3298             if (line.First() != '{' || line.Last() != '}')
3299             {
3300                 MyCommon.TraceOut("Invalid JSON (StatusArrived):" + Environment.NewLine + line);
3301                 return;
3302             }
3303
3304             var isDm = false;
3305
3306             try
3307             {
3308                 using (var jsonReader = JsonReaderWriterFactory.CreateJsonReader(Encoding.UTF8.GetBytes(line), XmlDictionaryReaderQuotas.Max))
3309                 {
3310                     var xElm = XElement.Load(jsonReader);
3311                     if (xElm.Element("friends") != null)
3312                     {
3313                         Debug.WriteLine("friends");
3314                         return;
3315                     }
3316                     else if (xElm.Element("delete") != null)
3317                     {
3318                         Debug.WriteLine("delete");
3319                         Int64 id;
3320                         XElement idElm;
3321                         if ((idElm = xElm.Element("delete").Element("direct_message")?.Element("id")) != null)
3322                         {
3323                             id = 0;
3324                             long.TryParse(idElm.Value, out id);
3325
3326                             this.PostDeleted?.Invoke(this, new PostDeletedEventArgs(id));
3327                         }
3328                         else if ((idElm = xElm.Element("delete").Element("status")?.Element("id")) != null)
3329                         {
3330                             id = 0;
3331                             long.TryParse(idElm.Value, out id);
3332
3333                             this.PostDeleted?.Invoke(this, new PostDeletedEventArgs(id));
3334                         }
3335                         else
3336                         {
3337                             MyCommon.TraceOut("delete:" + line);
3338                             return;
3339                         }
3340                         for (int i = this.StoredEvent.Count - 1; i >= 0; i--)
3341                         {
3342                             var sEvt = this.StoredEvent[i];
3343                             if (sEvt.Id == id && (sEvt.Event == "favorite" || sEvt.Event == "unfavorite"))
3344                             {
3345                                 this.StoredEvent.RemoveAt(i);
3346                             }
3347                         }
3348                         return;
3349                     }
3350                     else if (xElm.Element("limit") != null)
3351                     {
3352                         Debug.WriteLine(line);
3353                         return;
3354                     }
3355                     else if (xElm.Element("event") != null)
3356                     {
3357                         Debug.WriteLine("event: " + xElm.Element("event").Value);
3358                         CreateEventFromJson(line);
3359                         return;
3360                     }
3361                     else if (xElm.Element("direct_message") != null)
3362                     {
3363                         Debug.WriteLine("direct_message");
3364                         isDm = true;
3365                     }
3366                     else if (xElm.Element("retweeted_status") != null)
3367                     {
3368                         var sourceUserId = xElm.XPathSelectElement("/user/id_str").Value;
3369                         var targetUserId = xElm.XPathSelectElement("/retweeted_status/user/id_str").Value;
3370
3371                         // 自分に関係しないリツイートの場合は無視する
3372                         var selfUserId = this.UserId.ToString();
3373                         if (sourceUserId == selfUserId || targetUserId == selfUserId)
3374                         {
3375                             // 公式 RT をイベントとしても扱う
3376                             var evt = CreateEventFromRetweet(xElm);
3377                             if (evt != null)
3378                             {
3379                                 this.StoredEvent.Insert(0, evt);
3380
3381                                 this.UserStreamEventReceived?.Invoke(this, new UserStreamEventReceivedEventArgs(evt));
3382                             }
3383                         }
3384
3385                         // 従来通り公式 RT の表示も行うため return しない
3386                     }
3387                     else if (xElm.Element("scrub_geo") != null)
3388                     {
3389                         try
3390                         {
3391                             TabInformations.GetInstance().ScrubGeoReserve(long.Parse(xElm.Element("scrub_geo").Element("user_id").Value),
3392                                                                         long.Parse(xElm.Element("scrub_geo").Element("up_to_status_id").Value));
3393                         }
3394                         catch(Exception)
3395                         {
3396                             MyCommon.TraceOut("scrub_geo:" + line);
3397                         }
3398                         return;
3399                     }
3400                 }
3401
3402                 if (isDm)
3403                 {
3404                     CreateDirectMessagesFromJson(line, MyCommon.WORKERTYPE.UserStream, false);
3405                 }
3406                 else
3407                 {
3408                     CreatePostsFromJson("[" + line + "]", MyCommon.WORKERTYPE.Timeline, null, false);
3409                 }
3410             }
3411             catch (WebApiException ex)
3412             {
3413                 MyCommon.TraceOut(ex);
3414                 return;
3415             }
3416             catch(NullReferenceException)
3417             {
3418                 MyCommon.TraceOut("NullRef StatusArrived: " + line);
3419             }
3420
3421             this.NewPostFromStream?.Invoke(this, EventArgs.Empty);
3422         }
3423
3424         /// <summary>
3425         /// UserStreamsから受信した公式RTをイベントに変換します
3426         /// </summary>
3427         private FormattedEvent CreateEventFromRetweet(XElement xElm)
3428         {
3429             return new FormattedEvent
3430             {
3431                 Eventtype = MyCommon.EVENTTYPE.Retweet,
3432                 Event = "retweet",
3433                 CreatedAt = MyCommon.DateTimeParse(xElm.XPathSelectElement("/created_at").Value),
3434                 IsMe = xElm.XPathSelectElement("/user/id_str").Value == this.UserId.ToString(),
3435                 Username = xElm.XPathSelectElement("/user/screen_name").Value,
3436                 Target = string.Format("@{0}:{1}", new[]
3437                 {
3438                     xElm.XPathSelectElement("/retweeted_status/user/screen_name").Value,
3439                     xElm.XPathSelectElement("/retweeted_status/text").Value,
3440                 }),
3441                 Id = long.Parse(xElm.XPathSelectElement("/retweeted_status/id_str").Value),
3442             };
3443         }
3444
3445         private void CreateEventFromJson(string content)
3446         {
3447             TwitterStreamEvent eventData = null;
3448             try
3449             {
3450                 eventData = TwitterStreamEvent.ParseJson(content);
3451             }
3452             catch(SerializationException ex)
3453             {
3454                 MyCommon.TraceOut(ex, "Event Serialize Exception!" + Environment.NewLine + content);
3455             }
3456             catch(Exception ex)
3457             {
3458                 MyCommon.TraceOut(ex, "Event Exception!" + Environment.NewLine + content);
3459             }
3460
3461             var evt = new FormattedEvent();
3462             evt.CreatedAt = MyCommon.DateTimeParse(eventData.CreatedAt);
3463             evt.Event = eventData.Event;
3464             evt.Username = eventData.Source.ScreenName;
3465             evt.IsMe = evt.Username.ToLower().Equals(this.Username.ToLower());
3466
3467             MyCommon.EVENTTYPE eventType;
3468             eventTable.TryGetValue(eventData.Event, out eventType);
3469             evt.Eventtype = eventType;
3470
3471             TwitterStreamEvent<TwitterStatus> tweetEvent;
3472
3473             switch (eventData.Event)
3474             {
3475                 case "access_revoked":
3476                 case "access_unrevoked":
3477                 case "user_delete":
3478                 case "user_suspend":
3479                     return;
3480                 case "follow":
3481                     if (eventData.Target.ScreenName.ToLower().Equals(_uname))
3482                     {
3483                         if (!this.followerId.Contains(eventData.Source.Id)) this.followerId.Add(eventData.Source.Id);
3484                     }
3485                     else
3486                     {
3487                         return;    //Block後のUndoをすると、SourceとTargetが逆転したfollowイベントが帰ってくるため。
3488                     }
3489                     evt.Target = "";
3490                     break;
3491                 case "unfollow":
3492                     evt.Target = "@" + eventData.Target.ScreenName;
3493                     break;
3494                 case "favorited_retweet":
3495                 case "retweeted_retweet":
3496                     return;
3497                 case "favorite":
3498                 case "unfavorite":
3499                     tweetEvent = TwitterStreamEvent<TwitterStatus>.ParseJson(content);
3500                     evt.Target = "@" + tweetEvent.TargetObject.User.ScreenName + ":" + WebUtility.HtmlDecode(tweetEvent.TargetObject.Text);
3501                     evt.Id = tweetEvent.TargetObject.Id;
3502
3503                     if (SettingCommon.Instance.IsRemoveSameEvent)
3504                     {
3505                         if (this.StoredEvent.Any(ev => ev.Username == evt.Username && ev.Eventtype == evt.Eventtype && ev.Target == evt.Target))
3506                             return;
3507                     }
3508
3509                     var tabinfo = TabInformations.GetInstance();
3510
3511                     PostClass post;
3512                     var statusId = tweetEvent.TargetObject.Id;
3513                     if (!tabinfo.Posts.TryGetValue(statusId, out post))
3514                         break;
3515
3516                     if (eventData.Event == "favorite")
3517                     {
3518                         var favTab = tabinfo.GetTabByType(MyCommon.TabUsageType.Favorites);
3519                         if (!favTab.Contains(post.StatusId))
3520                             favTab.AddPostImmediately(post.StatusId, post.IsRead);
3521
3522                         if (tweetEvent.Source.Id == this.UserId)
3523                         {
3524                             post.IsFav = true;
3525                         }
3526                         else if (tweetEvent.Target.Id == this.UserId)
3527                         {
3528                             post.FavoritedCount++;
3529
3530                             if (SettingCommon.Instance.FavEventUnread)
3531                                 tabinfo.SetReadAllTab(post.StatusId, read: false);
3532                         }
3533                     }
3534                     else // unfavorite
3535                     {
3536                         if (tweetEvent.Source.Id == this.UserId)
3537                         {
3538                             post.IsFav = false;
3539                         }
3540                         else if (tweetEvent.Target.Id == this.UserId)
3541                         {
3542                             post.FavoritedCount = Math.Max(0, post.FavoritedCount - 1);
3543                         }
3544                     }
3545                     break;
3546                 case "quoted_tweet":
3547                     if (evt.IsMe) return;
3548
3549                     tweetEvent = TwitterStreamEvent<TwitterStatus>.ParseJson(content);
3550                     evt.Target = "@" + tweetEvent.TargetObject.User.ScreenName + ":" + WebUtility.HtmlDecode(tweetEvent.TargetObject.Text);
3551                     evt.Id = tweetEvent.TargetObject.Id;
3552
3553                     if (SettingCommon.Instance.IsRemoveSameEvent)
3554                     {
3555                         if (this.StoredEvent.Any(ev => ev.Username == evt.Username && ev.Eventtype == evt.Eventtype && ev.Target == evt.Target))
3556                             return;
3557                     }
3558                     break;
3559                 case "list_member_added":
3560                 case "list_member_removed":
3561                 case "list_created":
3562                 case "list_destroyed":
3563                 case "list_updated":
3564                 case "list_user_subscribed":
3565                 case "list_user_unsubscribed":
3566                     var listEvent = TwitterStreamEvent<TwitterList>.ParseJson(content);
3567                     evt.Target = listEvent.TargetObject.FullName;
3568                     break;
3569                 case "block":
3570                     if (!TabInformations.GetInstance().BlockIds.Contains(eventData.Target.Id)) TabInformations.GetInstance().BlockIds.Add(eventData.Target.Id);
3571                     evt.Target = "";
3572                     break;
3573                 case "unblock":
3574                     if (TabInformations.GetInstance().BlockIds.Contains(eventData.Target.Id)) TabInformations.GetInstance().BlockIds.Remove(eventData.Target.Id);
3575                     evt.Target = "";
3576                     break;
3577                 case "user_update":
3578                     evt.Target = "";
3579                     break;
3580                 
3581                 // Mute / Unmute
3582                 case "mute":
3583                     evt.Target = "@" + eventData.Target.ScreenName;
3584                     if (!TabInformations.GetInstance().MuteUserIds.Contains(eventData.Target.Id))
3585                     {
3586                         TabInformations.GetInstance().MuteUserIds.Add(eventData.Target.Id);
3587                     }
3588                     break;
3589                 case "unmute":
3590                     evt.Target = "@" + eventData.Target.ScreenName;
3591                     if (TabInformations.GetInstance().MuteUserIds.Contains(eventData.Target.Id))
3592                     {
3593                         TabInformations.GetInstance().MuteUserIds.Remove(eventData.Target.Id);
3594                     }
3595                     break;
3596
3597                 default:
3598                     MyCommon.TraceOut("Unknown Event:" + evt.Event + Environment.NewLine + content);
3599                     break;
3600             }
3601             this.StoredEvent.Insert(0, evt);
3602
3603             this.UserStreamEventReceived?.Invoke(this, new UserStreamEventReceivedEventArgs(evt));
3604         }
3605
3606         private void userStream_Started()
3607         {
3608             this.UserStreamStarted?.Invoke(this, EventArgs.Empty);
3609         }
3610
3611         private void userStream_Stopped()
3612         {
3613             this.UserStreamStopped?.Invoke(this, EventArgs.Empty);
3614         }
3615
3616         public bool UserStreamEnabled
3617         {
3618             get
3619             {
3620                 return userStream == null ? false : userStream.Enabled;
3621             }
3622         }
3623
3624         public void StartUserStream()
3625         {
3626             if (userStream != null)
3627             {
3628                 StopUserStream();
3629             }
3630             userStream = new TwitterUserstream(twCon);
3631             userStream.StatusArrived += userStream_StatusArrived;
3632             userStream.Started += userStream_Started;
3633             userStream.Stopped += userStream_Stopped;
3634             userStream.Start(this.AllAtReply, this.TrackWord);
3635         }
3636
3637         public void StopUserStream()
3638         {
3639             userStream?.Dispose();
3640             userStream = null;
3641             if (!MyCommon._endingFlag)
3642             {
3643                 this.UserStreamStopped?.Invoke(this, EventArgs.Empty);
3644             }
3645         }
3646
3647         public void ReconnectUserStream()
3648         {
3649             if (userStream != null)
3650             {
3651                 this.StartUserStream();
3652             }
3653         }
3654
3655         private class TwitterUserstream : IDisposable
3656         {
3657             public event Action<string> StatusArrived;
3658             public event Action Stopped;
3659             public event Action Started;
3660             private HttpTwitter twCon;
3661
3662             private Thread _streamThread;
3663             private bool _streamActive;
3664
3665             private bool _allAtreplies = false;
3666             private string _trackwords = "";
3667
3668             public TwitterUserstream(HttpTwitter twitterConnection)
3669             {
3670                 twCon = (HttpTwitter)twitterConnection.Clone();
3671             }
3672
3673             public void Start(bool allAtReplies, string trackwords)
3674             {
3675                 this.AllAtReplies = allAtReplies;
3676                 this.TrackWords = trackwords;
3677                 _streamActive = true;
3678                 if (_streamThread != null && _streamThread.IsAlive) return;
3679                 _streamThread = new Thread(UserStreamLoop);
3680                 _streamThread.Name = "UserStreamReceiver";
3681                 _streamThread.IsBackground = true;
3682                 _streamThread.Start();
3683             }
3684
3685             public bool Enabled
3686             {
3687                 get
3688                 {
3689                     return _streamActive;
3690                 }
3691             }
3692
3693             public bool AllAtReplies
3694             {
3695                 get
3696                 {
3697                     return _allAtreplies;
3698                 }
3699                 set
3700                 {
3701                     _allAtreplies = value;
3702                 }
3703             }
3704
3705             public string TrackWords
3706             {
3707                 get
3708                 {
3709                     return _trackwords;
3710                 }
3711                 set
3712                 {
3713                     _trackwords = value;
3714                 }
3715             }
3716
3717             private void UserStreamLoop()
3718             {
3719                 var sleepSec = 0;
3720                 do
3721                 {
3722                     Stream st = null;
3723                     StreamReader sr = null;
3724                     try
3725                     {
3726                         if (!MyCommon.IsNetworkAvailable())
3727                         {
3728                             sleepSec = 30;
3729                             continue;
3730                         }
3731
3732                         Started?.Invoke();
3733
3734                         var res = twCon.UserStream(ref st, _allAtreplies, _trackwords, Networking.GetUserAgentString());
3735
3736                         switch (res)
3737                         {
3738                             case HttpStatusCode.OK:
3739                                 Twitter.AccountState = MyCommon.ACCOUNT_STATE.Valid;
3740                                 break;
3741                             case HttpStatusCode.Unauthorized:
3742                                 Twitter.AccountState = MyCommon.ACCOUNT_STATE.Invalid;
3743                                 sleepSec = 120;
3744                                 continue;
3745                         }
3746
3747                         if (st == null)
3748                         {
3749                             sleepSec = 30;
3750                             //MyCommon.TraceOut("Stop:stream is null")
3751                             continue;
3752                         }
3753
3754                         sr = new StreamReader(st);
3755
3756                         while (_streamActive && !sr.EndOfStream && Twitter.AccountState == MyCommon.ACCOUNT_STATE.Valid)
3757                         {
3758                             StatusArrived?.Invoke(sr.ReadLine());
3759                             //this.LastTime = Now;
3760                         }
3761
3762                         if (sr.EndOfStream || Twitter.AccountState == MyCommon.ACCOUNT_STATE.Invalid)
3763                         {
3764                             sleepSec = 30;
3765                             //MyCommon.TraceOut("Stop:EndOfStream")
3766                             continue;
3767                         }
3768                         break;
3769                     }
3770                     catch(WebException ex)
3771                     {
3772                         if (ex.Status == WebExceptionStatus.Timeout)
3773                         {
3774                             sleepSec = 30;                        //MyCommon.TraceOut("Stop:Timeout")
3775                         }
3776                         else if (ex.Response != null && (int)((HttpWebResponse)ex.Response).StatusCode == 420)
3777                         {
3778                             //MyCommon.TraceOut("Stop:Connection Limit")
3779                             break;
3780                         }
3781                         else
3782                         {
3783                             sleepSec = 30;
3784                             //MyCommon.TraceOut("Stop:WebException " + ex.Status.ToString())
3785                         }
3786                     }
3787                     catch(ThreadAbortException)
3788                     {
3789                         break;
3790                     }
3791                     catch(IOException)
3792                     {
3793                         sleepSec = 30;
3794                         //MyCommon.TraceOut("Stop:IOException with Active." + Environment.NewLine + ex.Message)
3795                     }
3796                     catch(ArgumentException ex)
3797                     {
3798                         //System.ArgumentException: ストリームを読み取れませんでした。
3799                         //サーバー側もしくは通信経路上で切断された場合?タイムアウト頻発後発生
3800                         sleepSec = 30;
3801                         MyCommon.TraceOut(ex, "Stop:ArgumentException");
3802                     }
3803                     catch(Exception ex)
3804                     {
3805                         MyCommon.TraceOut("Stop:Exception." + Environment.NewLine + ex.Message);
3806                         MyCommon.ExceptionOut(ex);
3807                         sleepSec = 30;
3808                     }
3809                     finally
3810                     {
3811                         if (_streamActive)
3812                         {
3813                             Stopped?.Invoke();
3814                         }
3815                         twCon.RequestAbort();
3816                         sr?.Close();
3817                         if (sleepSec > 0)
3818                         {
3819                             var ms = 0;
3820                             while (_streamActive && ms < sleepSec * 1000)
3821                             {
3822                                 Thread.Sleep(500);
3823                                 ms += 500;
3824                             }
3825                         }
3826                         sleepSec = 0;
3827                     }
3828                 } while (this._streamActive);
3829
3830                 if (_streamActive)
3831                 {
3832                     Stopped?.Invoke();
3833                 }
3834                 MyCommon.TraceOut("Stop:EndLoop");
3835             }
3836
3837 #region "IDisposable Support"
3838             private bool disposedValue; // 重複する呼び出しを検出するには
3839
3840             // IDisposable
3841             protected virtual void Dispose(bool disposing)
3842             {
3843                 if (!this.disposedValue)
3844                 {
3845                     if (disposing)
3846                     {
3847                         _streamActive = false;
3848                         if (_streamThread != null && _streamThread.IsAlive)
3849                         {
3850                             _streamThread.Abort();
3851                         }
3852                     }
3853                 }
3854                 this.disposedValue = true;
3855             }
3856
3857             //protected Overrides void Finalize()
3858             //{
3859             //    // このコードを変更しないでください。クリーンアップ コードを上の Dispose(bool disposing) に記述します。
3860             //    Dispose(false)
3861             //    MyBase.Finalize()
3862             //}
3863
3864             // このコードは、破棄可能なパターンを正しく実装できるように Visual Basic によって追加されました。
3865             public void Dispose()
3866             {
3867                 // このコードを変更しないでください。クリーンアップ コードを上の Dispose(bool disposing) に記述します。
3868                 Dispose(true);
3869                 GC.SuppressFinalize(this);
3870             }
3871 #endregion
3872
3873         }
3874 #endregion
3875
3876 #region "IDisposable Support"
3877         private bool disposedValue; // 重複する呼び出しを検出するには
3878
3879         // IDisposable
3880         protected virtual void Dispose(bool disposing)
3881         {
3882             if (!this.disposedValue)
3883             {
3884                 if (disposing)
3885                 {
3886                     this.StopUserStream();
3887                 }
3888             }
3889             this.disposedValue = true;
3890         }
3891
3892         //protected Overrides void Finalize()
3893         //{
3894         //    // このコードを変更しないでください。クリーンアップ コードを上の Dispose(bool disposing) に記述します。
3895         //    Dispose(false)
3896         //    MyBase.Finalize()
3897         //}
3898
3899         // このコードは、破棄可能なパターンを正しく実装できるように Visual Basic によって追加されました。
3900         public void Dispose()
3901         {
3902             // このコードを変更しないでください。クリーンアップ コードを上の Dispose(bool disposing) に記述します。
3903             Dispose(true);
3904             GC.SuppressFinalize(this);
3905         }
3906 #endregion
3907     }
3908
3909     public class PostDeletedEventArgs : EventArgs
3910     {
3911         public long StatusId { get; }
3912
3913         public PostDeletedEventArgs(long statusId)
3914         {
3915             this.StatusId = statusId;
3916         }
3917     }
3918
3919     public class UserStreamEventReceivedEventArgs : EventArgs
3920     {
3921         public Twitter.FormattedEvent EventData { get; }
3922
3923         public UserStreamEventReceivedEventArgs(Twitter.FormattedEvent eventData)
3924         {
3925             this.EventData = eventData;
3926         }
3927     }
3928 }