OSDN Git Service

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